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,41 @@
|
|
|
1
|
+
// The inline TUI loop: drives the same Agent, rendering everything inline (composer follows the
|
|
2
|
+
// conversation, reply opens on the ◆ icon, spinner shows a "thinking" word while the model works).
|
|
3
|
+
|
|
4
|
+
import { stdout } from "node:process";
|
|
5
|
+
import type { Agent } from "./agent.ts";
|
|
6
|
+
import { setAsker } from "./tools.ts";
|
|
7
|
+
import { Tui } from "./tui.ts";
|
|
8
|
+
|
|
9
|
+
export async function runTui(agent: Agent, model: string): Promise<void> {
|
|
10
|
+
const tui = new Tui();
|
|
11
|
+
agent.setOnApprove(async (name, summary) => tui.confirm(`run ${name} ${summary}`));
|
|
12
|
+
setAsker((question, options) => tui.ask(question, options));
|
|
13
|
+
tui.start();
|
|
14
|
+
// Header as the first lines (scrolls away naturally as the conversation grows).
|
|
15
|
+
stdout.write(
|
|
16
|
+
`${"\x1b[38;5;214m"}█▀█ █▀▄ █▀█\n█▀█ █▄▀ █▀█\x1b[0m \x1b[2m${model}\x1b[0m\n` +
|
|
17
|
+
`\x1b[2mAsk me to build, edit, or explain code in ${process.cwd()}\x1b[0m\n\n`,
|
|
18
|
+
);
|
|
19
|
+
try {
|
|
20
|
+
for (;;) {
|
|
21
|
+
tui.setStatus(`${model} · ~${agent.contextTokens()} tok`);
|
|
22
|
+
const line = await tui.readLine();
|
|
23
|
+
if (line === null) break; // Ctrl+C/Ctrl+D at empty prompt
|
|
24
|
+
if (!line) continue;
|
|
25
|
+
if (line === "/exit" || line === "/quit") break;
|
|
26
|
+
const abort = new AbortController();
|
|
27
|
+
const steer: string[] = [];
|
|
28
|
+
tui.beginTurn(abort, steer);
|
|
29
|
+
try {
|
|
30
|
+
await agent.send(line, { signal: abort.signal, steer, onReplyStart: () => tui.replyStart() });
|
|
31
|
+
} catch (e) {
|
|
32
|
+
stdout.write(`\n\x1b[31m[error] ${e instanceof Error ? e.message : e}\x1b[0m`);
|
|
33
|
+
} finally {
|
|
34
|
+
tui.endTurn();
|
|
35
|
+
stdout.write("\n\n"); // breathing room before the next composer
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
} finally {
|
|
39
|
+
tui.stop();
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
// Inline terminal UI for the ada REPL — no scroll region, no pinned footer. Everything flows
|
|
2
|
+
// inline like a chat: the composer is drawn right after the last reply (so there's never a void),
|
|
3
|
+
// the user's line is committed as a dim full-width bar, and ada's reply begins on the ◆ icon's
|
|
4
|
+
// line. While the model is thinking, a spinner cycles a random "processing" word. Raw-mode input
|
|
5
|
+
// is a small state machine:
|
|
6
|
+
// line — reading a user message (Enter submits; ↑/↓ history)
|
|
7
|
+
// turn — agent running (Esc/Ctrl+C interrupts; type+Enter queues a steer)
|
|
8
|
+
// confirm — an approval prompt awaiting y / a / n
|
|
9
|
+
|
|
10
|
+
import { stdin, stdout } from "node:process";
|
|
11
|
+
|
|
12
|
+
const GOLD = "\x1b[38;5;214m"; // ada accent (xterm 214)
|
|
13
|
+
const DIM = "\x1b[2m";
|
|
14
|
+
const RST = "\x1b[0m";
|
|
15
|
+
const SPIN = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
16
|
+
// Playful gerunds shown while the model works (à la Claude's "Cogitating…").
|
|
17
|
+
const WORDS = [
|
|
18
|
+
"Cogitating", "Pondering", "Noodling", "Percolating", "Ruminating", "Conjuring",
|
|
19
|
+
"Finagling", "Tinkering", "Scheming", "Untangling", "Marinating", "Synthesizing",
|
|
20
|
+
"Spelunking", "Vibing", "Hatching", "Brewing", "Mulling", "Crunching", "Simmering", "Whittling",
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
type Mode = "idle" | "line" | "turn" | "confirm";
|
|
24
|
+
|
|
25
|
+
/** Claude-style full-width user bar, e.g. " › hi" on a dim background. */
|
|
26
|
+
export function userBar(text: string, cols: number): string {
|
|
27
|
+
const visible = 3 + text.length; // " › " + text
|
|
28
|
+
const pad = visible >= cols ? "" : " ".repeat(cols - visible);
|
|
29
|
+
return `\x1b[48;5;238m ${GOLD}›${RST}\x1b[97m ${text}${pad}${RST}`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export class Tui {
|
|
33
|
+
private buf = "";
|
|
34
|
+
private status = "";
|
|
35
|
+
private history: string[] = [];
|
|
36
|
+
private hist = -1;
|
|
37
|
+
private mode: Mode = "idle";
|
|
38
|
+
private spin = 0;
|
|
39
|
+
private word = WORDS[0]!;
|
|
40
|
+
private thinkTimer: ReturnType<typeof setInterval> | null = null;
|
|
41
|
+
private thinking = false;
|
|
42
|
+
|
|
43
|
+
private lineResolve: ((s: string | null) => void) | null = null;
|
|
44
|
+
private confirmResolve: ((s: "yes" | "all" | "no") => void) | null = null;
|
|
45
|
+
private abort: AbortController | null = null;
|
|
46
|
+
private steer: string[] | null = null;
|
|
47
|
+
private onData = (b: Buffer): void => this.key(b.toString("utf8"));
|
|
48
|
+
|
|
49
|
+
start(): void {
|
|
50
|
+
if (stdin.isTTY) stdin.setRawMode(true);
|
|
51
|
+
stdin.resume();
|
|
52
|
+
stdin.on("data", this.onData);
|
|
53
|
+
stdout.write("\x1b[?25l"); // hide the real cursor; the composer draws a fake block
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
stop(): void {
|
|
57
|
+
this.stopThinking();
|
|
58
|
+
stdin.off("data", this.onData);
|
|
59
|
+
if (stdin.isTTY) stdin.setRawMode(false);
|
|
60
|
+
stdin.pause(); // unref stdin so the process can exit
|
|
61
|
+
stdout.write("\x1b[?25h\n"); // show cursor, fresh line
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
setStatus(s: string): void {
|
|
65
|
+
this.status = s;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Read one user line in an inline composer. Resolves null on Ctrl+C/Ctrl+D at an empty prompt. */
|
|
69
|
+
readLine(): Promise<string | null> {
|
|
70
|
+
this.mode = "line";
|
|
71
|
+
this.buf = "";
|
|
72
|
+
this.hist = -1;
|
|
73
|
+
this.renderComposer();
|
|
74
|
+
return new Promise((res) => (this.lineResolve = res));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Ask the user a question mid-turn (for the ask_user tool); returns their answer. */
|
|
78
|
+
async ask(question: string, options?: string[]): Promise<string> {
|
|
79
|
+
this.stopThinking();
|
|
80
|
+
stdout.write(`\x1b[36m? ${question}\x1b[0m\n`);
|
|
81
|
+
if (options?.length) stdout.write(`${options.map((o, i) => ` ${i + 1}. ${o}`).join("\n")}\n`);
|
|
82
|
+
const ans = ((await this.readLine()) ?? "").trim();
|
|
83
|
+
this.mode = "turn";
|
|
84
|
+
if (options?.length) {
|
|
85
|
+
const n = Number(ans);
|
|
86
|
+
if (Number.isInteger(n) && n >= 1 && n <= options.length) return options[n - 1]!;
|
|
87
|
+
}
|
|
88
|
+
return ans;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
beginTurn(abort: AbortController, steer: string[]): void {
|
|
92
|
+
this.mode = "turn";
|
|
93
|
+
this.abort = abort;
|
|
94
|
+
this.steer = steer;
|
|
95
|
+
this.buf = "";
|
|
96
|
+
this.startThinking();
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
endTurn(): void {
|
|
100
|
+
this.stopThinking();
|
|
101
|
+
this.mode = "idle";
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** First visible output of the turn: drop the spinner and open the reply on the ◆ line. */
|
|
105
|
+
replyStart(): void {
|
|
106
|
+
this.stopThinking();
|
|
107
|
+
stdout.write(`${GOLD}◆${RST} `);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Approval prompt drawn inline; resolves on y / a / n. */
|
|
111
|
+
confirm(label: string): Promise<"yes" | "all" | "no"> {
|
|
112
|
+
this.stopThinking();
|
|
113
|
+
this.mode = "confirm";
|
|
114
|
+
stdout.write(`${GOLD}?${RST} ${label} ${DIM}[y]es / [a]ll / [N]o${RST} `);
|
|
115
|
+
return new Promise((res) => (this.confirmResolve = res));
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// ---- internals ----
|
|
119
|
+
private renderComposer(): void {
|
|
120
|
+
const cols = stdout.columns || 80;
|
|
121
|
+
const max = Math.max(8, cols - 3);
|
|
122
|
+
const shown = this.buf.length > max ? `…${this.buf.slice(this.buf.length - max + 1)}` : this.buf;
|
|
123
|
+
stdout.write(`\r\x1b[2K${GOLD}›${RST} ${shown}\x1b[7m \x1b[0m`); // trailing reverse-video = block cursor
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
private commitUser(text: string): void {
|
|
127
|
+
stdout.write(`\r\x1b[2K${userBar(text, stdout.columns || 80)}\n`); // composer line becomes the bar
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
private startThinking(): void {
|
|
131
|
+
this.thinking = true;
|
|
132
|
+
this.word = WORDS[Math.floor(Math.random() * WORDS.length)]!;
|
|
133
|
+
this.renderThinking();
|
|
134
|
+
this.thinkTimer = setInterval(() => {
|
|
135
|
+
this.spin = (this.spin + 1) % SPIN.length;
|
|
136
|
+
if (this.spin === 0) this.word = WORDS[Math.floor(Math.random() * WORDS.length)]!;
|
|
137
|
+
this.renderThinking();
|
|
138
|
+
}, 90);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
private renderThinking(): void {
|
|
142
|
+
const tail = this.status ? ` ${DIM}${this.status} · esc to interrupt${RST}` : ` ${DIM}esc to interrupt${RST}`;
|
|
143
|
+
stdout.write(`\r\x1b[2K${GOLD}${SPIN[this.spin]}${RST} ${DIM}${this.word}…${RST}${tail}`);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
private stopThinking(): void {
|
|
147
|
+
if (this.thinkTimer) {
|
|
148
|
+
clearInterval(this.thinkTimer);
|
|
149
|
+
this.thinkTimer = null;
|
|
150
|
+
}
|
|
151
|
+
if (this.thinking) {
|
|
152
|
+
stdout.write("\r\x1b[2K"); // erase the spinner line, leave cursor at column 0
|
|
153
|
+
this.thinking = false;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
private key(s: string): void {
|
|
158
|
+
if (this.mode === "confirm") {
|
|
159
|
+
const k = s.toLowerCase();
|
|
160
|
+
if (k === "y") this.resolveConfirm("yes");
|
|
161
|
+
else if (k === "a") this.resolveConfirm("all");
|
|
162
|
+
else if (k === "n" || k === "\r" || k === "\n" || k === "\x1b") this.resolveConfirm("no");
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (s === "\x03") {
|
|
167
|
+
// Ctrl+C
|
|
168
|
+
if (this.mode === "turn") this.abort?.abort();
|
|
169
|
+
else if (this.lineResolve) {
|
|
170
|
+
stdout.write("\n");
|
|
171
|
+
this.lineResolve(this.buf ? "" : null); // empty prompt → exit
|
|
172
|
+
}
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
if (s === "\x1b") {
|
|
176
|
+
if (this.mode === "turn") this.abort?.abort();
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
if (s === "\x1b[A" || s === "\x1b[B") {
|
|
180
|
+
if (this.mode === "line" && this.history.length) {
|
|
181
|
+
if (s === "\x1b[A") this.hist = this.hist < 0 ? this.history.length - 1 : Math.max(0, this.hist - 1);
|
|
182
|
+
else this.hist = this.hist < 0 ? -1 : this.hist + 1;
|
|
183
|
+
this.buf = this.hist >= 0 && this.hist < this.history.length ? this.history[this.hist]! : "";
|
|
184
|
+
this.renderComposer();
|
|
185
|
+
}
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
if (s.startsWith("\x1b")) return; // ignore other escape sequences
|
|
189
|
+
|
|
190
|
+
for (const ch of s) {
|
|
191
|
+
if (ch === "\r" || ch === "\n") {
|
|
192
|
+
const text = this.buf.trim();
|
|
193
|
+
this.buf = "";
|
|
194
|
+
if (this.mode === "line") {
|
|
195
|
+
if (text) {
|
|
196
|
+
this.history.push(text);
|
|
197
|
+
this.commitUser(text); // echo exactly once, as the bar
|
|
198
|
+
} else {
|
|
199
|
+
stdout.write("\r\x1b[2K");
|
|
200
|
+
}
|
|
201
|
+
this.hist = -1;
|
|
202
|
+
this.lineResolve?.(text);
|
|
203
|
+
} else if (this.mode === "turn" && text) {
|
|
204
|
+
this.steer?.push(text);
|
|
205
|
+
stdout.write(`${DIM} ↳ queued: ${text}${RST}\n`); // steer is captured blind, echoed on submit
|
|
206
|
+
}
|
|
207
|
+
} else if (ch === "\x7f" || ch === "\b") {
|
|
208
|
+
this.buf = this.buf.slice(0, -1);
|
|
209
|
+
if (this.mode === "line") this.renderComposer();
|
|
210
|
+
} else if (ch >= " ") {
|
|
211
|
+
this.buf += ch;
|
|
212
|
+
if (this.mode === "line") this.renderComposer();
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
private resolveConfirm(d: "yes" | "all" | "no"): void {
|
|
218
|
+
this.mode = "turn";
|
|
219
|
+
const r = this.confirmResolve;
|
|
220
|
+
this.confirmResolve = null;
|
|
221
|
+
stdout.write("\n"); // finalize the prompt line
|
|
222
|
+
r?.(d);
|
|
223
|
+
}
|
|
224
|
+
}
|
package/src/sdk/index.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// Typed client SDK for the ada HTTP API (started with `ada serve`). Drive ada programmatically:
|
|
2
|
+
//
|
|
3
|
+
// import { createClient } from "ada/sdk";
|
|
4
|
+
// const ada = createClient("http://localhost:8788");
|
|
5
|
+
// const { text } = await ada.prompt("list the files in this project");
|
|
6
|
+
|
|
7
|
+
export interface PromptResult {
|
|
8
|
+
text: string;
|
|
9
|
+
usage?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface AdaClient {
|
|
13
|
+
/** Send a prompt; runs a fresh agent turn server-side and returns its final text. */
|
|
14
|
+
prompt(text: string, opts?: { model?: string }): Promise<PromptResult>;
|
|
15
|
+
/** Server health + the default model. */
|
|
16
|
+
health(): Promise<{ ok: boolean; model?: string }>;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function createClient(baseUrl = "http://localhost:8788"): AdaClient {
|
|
20
|
+
const url = baseUrl.replace(/\/+$/, "");
|
|
21
|
+
return {
|
|
22
|
+
async prompt(text, opts) {
|
|
23
|
+
const res = await fetch(`${url}/v1/prompt`, {
|
|
24
|
+
method: "POST",
|
|
25
|
+
headers: { "content-type": "application/json" },
|
|
26
|
+
body: JSON.stringify({ text, model: opts?.model }),
|
|
27
|
+
});
|
|
28
|
+
if (!res.ok) throw new Error(`ada ${res.status}: ${await res.text().catch(() => res.statusText)}`);
|
|
29
|
+
return (await res.json()) as PromptResult;
|
|
30
|
+
},
|
|
31
|
+
async health() {
|
|
32
|
+
const res = await fetch(`${url}/health`);
|
|
33
|
+
return (await res.json()) as { ok: boolean; model?: string };
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
}
|
package/src/selfcheck.ts
ADDED
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
// Offline self-check: tools, session persistence, and routing. No network, no API key.
|
|
2
|
+
// Run with: npm run selfcheck
|
|
3
|
+
|
|
4
|
+
import assert from "node:assert/strict";
|
|
5
|
+
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
6
|
+
import { tmpdir } from "node:os";
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
import { estimateTokens, isContextOverflowError, planCut } from "./client/compaction.ts";
|
|
9
|
+
import { loadImage } from "./client/image.ts";
|
|
10
|
+
import { expandPrompt } from "./client/prompts.ts";
|
|
11
|
+
import { MarkdownStreamer, highlight, renderEditDiff } from "./client/render.ts";
|
|
12
|
+
import { Session, list } from "./client/session.ts";
|
|
13
|
+
import { loadSkills, registerSkillTool, routeConfident } from "./client/skills.ts";
|
|
14
|
+
import { describeCall, parseTextToolCalls, permPhrase, readIntegrationDocs, soleIntegration, writeProjectSkills } from "./client/agent.ts";
|
|
15
|
+
import { userBar } from "./client/tui.ts";
|
|
16
|
+
import { configuredServers, listConnectors, loadMcpServers } from "./client/mcp.ts";
|
|
17
|
+
import { confidentSkill, rankSkills } from "./client/skill-router.ts";
|
|
18
|
+
import { getDiagnostics } from "./client/lsp.ts";
|
|
19
|
+
import { snapshot } from "./client/snapshot.ts";
|
|
20
|
+
import { renderJobs, startJob } from "./client/background.ts";
|
|
21
|
+
import { formatFile, htmlToText, isDestructive, registerTool, setAsker, toolByName } from "./client/tools.ts";
|
|
22
|
+
import * as checkpoint from "./client/checkpoint.ts";
|
|
23
|
+
import { renderTodos, setTodos } from "./client/todos.ts";
|
|
24
|
+
import { deleteCredential, getCredential, setCredential } from "./server/credentials.ts";
|
|
25
|
+
import { isAllowed } from "./server/identity.ts";
|
|
26
|
+
import { route } from "./server/router.ts";
|
|
27
|
+
|
|
28
|
+
function tool(name: string) {
|
|
29
|
+
const t = toolByName.get(name);
|
|
30
|
+
if (!t) throw new Error(`missing tool: ${name}`);
|
|
31
|
+
return t;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function main(): Promise<void> {
|
|
35
|
+
// --- tools: write -> edit -> read round-trip ---
|
|
36
|
+
const dir = join(tmpdir(), `ada-selfcheck-${Date.now()}`);
|
|
37
|
+
const file = join(dir, "a.txt");
|
|
38
|
+
|
|
39
|
+
let r = await tool("write_file").run({ path: file, content: "hello world" });
|
|
40
|
+
assert.ok(!r.isError, r.output);
|
|
41
|
+
r = await tool("edit_file").run({ path: file, old_text: "world", new_text: "ada" });
|
|
42
|
+
assert.ok(!r.isError, r.output);
|
|
43
|
+
r = await tool("read_file").run({ path: file });
|
|
44
|
+
assert.equal(r.output, "hello ada");
|
|
45
|
+
|
|
46
|
+
// ambiguous edit must error
|
|
47
|
+
await tool("write_file").run({ path: file, content: "x x" });
|
|
48
|
+
r = await tool("edit_file").run({ path: file, old_text: "x", new_text: "y" });
|
|
49
|
+
assert.ok(r.isError, "ambiguous edit should error");
|
|
50
|
+
|
|
51
|
+
// missing read must error
|
|
52
|
+
r = await tool("read_file").run({ path: join(dir, "nope.txt") });
|
|
53
|
+
assert.ok(r.isError, "missing read should error");
|
|
54
|
+
|
|
55
|
+
// bash
|
|
56
|
+
r = await tool("bash").run({ command: "echo hi" });
|
|
57
|
+
assert.ok(r.output.includes("hi"), r.output);
|
|
58
|
+
|
|
59
|
+
// grep / ls / glob
|
|
60
|
+
await tool("write_file").run({ path: join(dir, "hello.txt"), content: "alpha\nNEEDLE here\nbeta" });
|
|
61
|
+
const g = await tool("grep").run({ path: dir, pattern: "NEEDLE" });
|
|
62
|
+
assert.ok(g.output.includes("NEEDLE"), g.output);
|
|
63
|
+
const l = await tool("ls").run({ path: dir });
|
|
64
|
+
assert.ok(l.output.includes("hello.txt"), l.output);
|
|
65
|
+
const gl = await tool("glob").run({ pattern: "src/selfcheck.ts" });
|
|
66
|
+
assert.ok(gl.output.includes("selfcheck.ts"), gl.output);
|
|
67
|
+
|
|
68
|
+
// read offset/limit
|
|
69
|
+
await tool("write_file").run({ path: join(dir, "lines.txt"), content: "L1\nL2\nL3\nL4" });
|
|
70
|
+
const ol = await tool("read_file").run({ path: join(dir, "lines.txt"), offset: 2, limit: 2 });
|
|
71
|
+
assert.equal(ol.output, "L2\nL3");
|
|
72
|
+
|
|
73
|
+
// multi-edit
|
|
74
|
+
await tool("write_file").run({ path: join(dir, "m.txt"), content: "aaa bbb ccc" });
|
|
75
|
+
r = await tool("edit_file").run({
|
|
76
|
+
path: join(dir, "m.txt"),
|
|
77
|
+
edits: [
|
|
78
|
+
{ old_text: "aaa", new_text: "AAA" },
|
|
79
|
+
{ old_text: "ccc", new_text: "CCC" },
|
|
80
|
+
],
|
|
81
|
+
});
|
|
82
|
+
assert.ok(!r.isError, r.output);
|
|
83
|
+
r = await tool("read_file").run({ path: join(dir, "m.txt") });
|
|
84
|
+
assert.equal(r.output, "AAA bbb CCC");
|
|
85
|
+
|
|
86
|
+
// CRLF preservation: file uses \r\n, edit's old_text uses \n
|
|
87
|
+
const crlf = join(dir, "crlf.txt");
|
|
88
|
+
await tool("write_file").run({ path: crlf, content: "one\r\ntwo\r\nthree" });
|
|
89
|
+
r = await tool("edit_file").run({ path: crlf, old_text: "two", new_text: "TWO" });
|
|
90
|
+
assert.ok(!r.isError, r.output);
|
|
91
|
+
r = await tool("read_file").run({ path: crlf });
|
|
92
|
+
assert.ok(r.output.includes("\r\n") && r.output.includes("TWO"), JSON.stringify(r.output));
|
|
93
|
+
|
|
94
|
+
rmSync(dir, { recursive: true, force: true });
|
|
95
|
+
|
|
96
|
+
// --- session append -> load round-trip ---
|
|
97
|
+
const s = Session.create();
|
|
98
|
+
s.append({ role: "user", content: "hello" });
|
|
99
|
+
s.append({ role: "assistant", content: "hi there" });
|
|
100
|
+
const loaded = s.load();
|
|
101
|
+
assert.equal(loaded.length, 2);
|
|
102
|
+
assert.equal(loaded[0]!.content, "hello");
|
|
103
|
+
rmSync(s.file, { force: true });
|
|
104
|
+
|
|
105
|
+
// --- branching: fork seeds messages, records parent, load skips __meta ---
|
|
106
|
+
const parent = Session.create();
|
|
107
|
+
parent.append({ role: "user", content: "p1" });
|
|
108
|
+
const branch = Session.fork(parent.file, [
|
|
109
|
+
{ role: "user", content: "p1" },
|
|
110
|
+
{ role: "assistant", content: "a1" },
|
|
111
|
+
]);
|
|
112
|
+
const bl = branch.load();
|
|
113
|
+
assert.equal(bl.length, 2, "fork load skips the __meta line");
|
|
114
|
+
assert.equal(bl[0]!.content, "p1");
|
|
115
|
+
const bm = list().find((m) => m.file === branch.file);
|
|
116
|
+
assert.ok(bm?.parent === parent.file, "branch records its parent");
|
|
117
|
+
rmSync(parent.file, { force: true });
|
|
118
|
+
rmSync(branch.file, { force: true });
|
|
119
|
+
|
|
120
|
+
// --- router prefix mapping ---
|
|
121
|
+
assert.equal(route("gpt-4o"), "openai");
|
|
122
|
+
assert.equal(route("o3-mini"), "openai");
|
|
123
|
+
assert.equal(route("claude-opus-4-8"), "anthropic");
|
|
124
|
+
assert.equal(route("gemini-2.5-pro"), "google");
|
|
125
|
+
assert.equal(route("mistral-large-latest"), "mistral");
|
|
126
|
+
assert.equal(route("grok-2"), "xai");
|
|
127
|
+
assert.equal(route("deepseek-chat"), "deepseek");
|
|
128
|
+
assert.equal(route("qwen-max"), "dashscope");
|
|
129
|
+
assert.equal(route("qwq-32b"), "dashscope");
|
|
130
|
+
assert.equal(route("qwen/qwen-2.5-72b-instruct"), "openrouter"); // namespaced id stays on OpenRouter
|
|
131
|
+
assert.equal(route("gemma4:latest"), "ollama"); // local Ollama "model:tag"
|
|
132
|
+
assert.equal(route("mistralai/mistral-7b:free"), "openrouter"); // slash wins over colon
|
|
133
|
+
assert.equal(route("meta-llama/llama-3.1-70b"), "openrouter");
|
|
134
|
+
assert.equal(route("anything", "mistral"), "mistral");
|
|
135
|
+
|
|
136
|
+
// --- compaction ---
|
|
137
|
+
assert.ok(estimateTokens([{ role: "user", content: "hello" }] as never) > 0);
|
|
138
|
+
assert.ok(isContextOverflowError(new Error("maximum context length exceeded")));
|
|
139
|
+
assert.ok(!isContextOverflowError(new Error("invalid api key")));
|
|
140
|
+
const convo = [
|
|
141
|
+
{ role: "system", content: "sys" },
|
|
142
|
+
{ role: "user", content: "u1" },
|
|
143
|
+
{ role: "assistant", content: null, tool_calls: [{ id: "c1", type: "function", function: { name: "bash", arguments: "{}" } }] },
|
|
144
|
+
{ role: "tool", tool_call_id: "c1", content: "out" },
|
|
145
|
+
{ role: "assistant", content: "a1" },
|
|
146
|
+
{ role: "user", content: "u2" },
|
|
147
|
+
{ role: "assistant", content: "a2" },
|
|
148
|
+
{ role: "user", content: "u3" },
|
|
149
|
+
{ role: "assistant", content: "a3" },
|
|
150
|
+
];
|
|
151
|
+
const plan = planCut(convo as never, 2);
|
|
152
|
+
assert.ok(plan, "should plan a cut");
|
|
153
|
+
assert.equal(plan!.system!.role, "system");
|
|
154
|
+
assert.equal(plan!.tail[0]!.role, "user"); // tail starts on a user boundary — tool pairs never split
|
|
155
|
+
|
|
156
|
+
// --- rendering ---
|
|
157
|
+
const diff = renderEditDiff("f.ts", "old line", "new line");
|
|
158
|
+
assert.ok(diff.includes("old line") && diff.includes("new line"), diff);
|
|
159
|
+
const ms = new MarkdownStreamer();
|
|
160
|
+
const rendered = ms.push("# Title\n- item\n") + ms.end();
|
|
161
|
+
assert.ok(rendered.includes("Title") && rendered.includes("item"), rendered);
|
|
162
|
+
const hl = highlight('const x = "hi" // c');
|
|
163
|
+
assert.ok(hl.includes("\x1b[") && hl.includes("const"), hl); // keywords/strings/comments colored
|
|
164
|
+
|
|
165
|
+
// --- prompt templates ---
|
|
166
|
+
const pm = new Map([["fix", "Fix $1 carefully. All: $ARGUMENTS"]]);
|
|
167
|
+
assert.equal(expandPrompt(pm, "/fix foo.ts it crashes"), "Fix foo.ts carefully. All: foo.ts it crashes");
|
|
168
|
+
assert.equal(expandPrompt(pm, "/unknown x"), null);
|
|
169
|
+
assert.equal(expandPrompt(pm, "hello"), null);
|
|
170
|
+
|
|
171
|
+
// --- extensibility: dynamic tool registration + skills ---
|
|
172
|
+
registerTool({
|
|
173
|
+
name: "__demo",
|
|
174
|
+
description: "demo",
|
|
175
|
+
parameters: { type: "object", properties: {} },
|
|
176
|
+
needsApproval: false,
|
|
177
|
+
async run() {
|
|
178
|
+
return { output: "ok" };
|
|
179
|
+
},
|
|
180
|
+
});
|
|
181
|
+
assert.ok(toolByName.get("__demo"), "registerTool adds a dynamic tool");
|
|
182
|
+
registerSkillTool([{ name: "demo", description: "d", path: "nope" }]);
|
|
183
|
+
assert.ok(toolByName.get("use_skill"), "registerSkillTool exposes use_skill");
|
|
184
|
+
|
|
185
|
+
// --- credential store round-trip ---
|
|
186
|
+
await setCredential("__selfcheck", { type: "api_key", key: "sk-test" });
|
|
187
|
+
assert.equal(getCredential("__selfcheck")?.key, "sk-test");
|
|
188
|
+
await deleteCredential("__selfcheck");
|
|
189
|
+
assert.equal(getCredential("__selfcheck"), undefined);
|
|
190
|
+
|
|
191
|
+
// --- multimodal: image file → data url ---
|
|
192
|
+
const imgPath = join(tmpdir(), `ada-img-${Date.now()}.png`);
|
|
193
|
+
writeFileSync(imgPath, Buffer.from([0x89, 0x50, 0x4e, 0x47]));
|
|
194
|
+
const img = loadImage(imgPath);
|
|
195
|
+
assert.ok(img && img.dataUrl.startsWith("data:image/png;base64,"), "loadImage → png data url");
|
|
196
|
+
rmSync(imgPath, { force: true });
|
|
197
|
+
|
|
198
|
+
// --- checkpoint undo round-trip ---
|
|
199
|
+
const cpFile = join(tmpdir(), `ada-cp-${Date.now()}.txt`);
|
|
200
|
+
writeFileSync(cpFile, "v1");
|
|
201
|
+
checkpoint.record(cpFile);
|
|
202
|
+
writeFileSync(cpFile, "v2");
|
|
203
|
+
checkpoint.undoAll();
|
|
204
|
+
assert.equal(readFileSync(cpFile, "utf8"), "v1", "undo restores the original content");
|
|
205
|
+
rmSync(cpFile, { force: true });
|
|
206
|
+
|
|
207
|
+
// --- todos + destructive detection ---
|
|
208
|
+
setTodos([{ text: "alpha", status: "done" }, { text: "beta", status: "todo" }]);
|
|
209
|
+
assert.ok(renderTodos().includes("alpha") && renderTodos().includes("beta"), "todos render");
|
|
210
|
+
assert.ok(isDestructive("rm -rf /tmp/x"), "rm -rf is destructive");
|
|
211
|
+
assert.ok(!isDestructive("ls -la"), "ls is not destructive");
|
|
212
|
+
|
|
213
|
+
// --- web_fetch HTML→text + tools registered ---
|
|
214
|
+
const ht = htmlToText("<h1>Hi</h1><p>a & b</p><script>x()</script><ul><li>one</li></ul>");
|
|
215
|
+
assert.ok(/Hi/.test(ht) && /a & b/.test(ht) && /- one/.test(ht) && !/x\(\)/.test(ht), "htmlToText strips tags/scripts, decodes entities");
|
|
216
|
+
assert.ok(toolByName.has("web_fetch") && toolByName.has("web_search"), "web tools registered");
|
|
217
|
+
assert.equal(formatFile(join(tmpdir(), "x.go")), false, "formatFile is a safe no-op when untrusted/no formatter (never throws)");
|
|
218
|
+
assert.ok(toolByName.has("lsp_diagnostics"), "lsp_diagnostics tool registered");
|
|
219
|
+
assert.deepEqual(await getDiagnostics(join(tmpdir(), "x.ts")), [], "getDiagnostics no-ops when untrusted/no server (never throws)");
|
|
220
|
+
const bashRun = await toolByName.get("bash")!.run({ command: "echo pty-probe-123" });
|
|
221
|
+
assert.ok(/pty-probe-123/.test(bashRun.output) && /exit 0/.test(bashRun.output), `bash runs a command (PTY): ${bashRun.output.slice(0, 60)}`);
|
|
222
|
+
|
|
223
|
+
// --- apply_patch: create → update → delete across files ---
|
|
224
|
+
const ap = toolByName.get("apply_patch")!;
|
|
225
|
+
const apDir = join(tmpdir(), `ada-ap-${process.pid}`);
|
|
226
|
+
mkdirSync(apDir, { recursive: true });
|
|
227
|
+
const apFile = join(apDir, "a.txt");
|
|
228
|
+
assert.ok(!(await ap.run({ files: [{ path: apFile, action: "create", content: "hello\n" }] })).isError && existsSync(apFile), "apply_patch create");
|
|
229
|
+
await ap.run({ files: [{ path: apFile, action: "update", edits: [{ old_text: "hello", new_text: "world" }] }] });
|
|
230
|
+
assert.ok(/world/.test(readFileSync(apFile, "utf8")), "apply_patch update");
|
|
231
|
+
await ap.run({ files: [{ path: apFile, action: "delete" }] });
|
|
232
|
+
assert.ok(!existsSync(apFile), "apply_patch delete");
|
|
233
|
+
rmSync(apDir, { recursive: true, force: true });
|
|
234
|
+
|
|
235
|
+
// --- ask_user via a stub asker ---
|
|
236
|
+
const askTool = toolByName.get("ask_user")!;
|
|
237
|
+
setAsker(async (_q, opts) => (opts ? opts[0]! : "the-answer"));
|
|
238
|
+
assert.ok(/the-answer/.test((await askTool.run({ question: "?" })).output), "ask_user returns the answer");
|
|
239
|
+
assert.ok(/picked-A/.test((await askTool.run({ question: "?", options: ["picked-A", "B"] })).output), "ask_user with options");
|
|
240
|
+
setAsker(null);
|
|
241
|
+
assert.equal((await askTool.run({ question: "?" })).isError, true, "ask_user errors when no asker is installed");
|
|
242
|
+
|
|
243
|
+
// --- grep still works (rg fast path falls back to the JS scan when rg is absent) ---
|
|
244
|
+
assert.ok(/tools\.ts/.test((await toolByName.get("grep")!.run({ pattern: "export const tools", path: "src/client" })).output), "grep finds matches");
|
|
245
|
+
|
|
246
|
+
// --- workspace snapshot returns a git tree SHA (or null outside a repo); never throws ---
|
|
247
|
+
const snap = snapshot();
|
|
248
|
+
assert.ok(snap === null || /^[0-9a-f]{40}$/.test(snap), "snapshot returns a tree SHA");
|
|
249
|
+
|
|
250
|
+
// --- approval context: readable call descriptions + plain-words permission phrases ---
|
|
251
|
+
assert.equal(describeCall("bash", { command: 'dir "C:\\x" /b' }).detail, 'dir "C:\\x" /b', "bash → shows the command, not JSON");
|
|
252
|
+
assert.equal(describeCall("read_file", { path: "a.ts" }).label, "read", "read_file → 'read'");
|
|
253
|
+
assert.equal(describeCall("merchant__list_products", {}).label, "merchant", "MCP tool → connector name as label");
|
|
254
|
+
assert.ok(permPhrase("bash", true).startsWith("⚠"), "destructive bash phrase is flagged");
|
|
255
|
+
assert.equal(permPhrase("write_file", false), "create or modify files on disk", "write phrase");
|
|
256
|
+
assert.ok(permPhrase("merchant__x", false).includes("connector"), "MCP phrase mentions the connector");
|
|
257
|
+
|
|
258
|
+
// --- background job runs and reports ---
|
|
259
|
+
const jid = startJob("selfcheck job", async () => "job-done-ok");
|
|
260
|
+
await new Promise((r) => setTimeout(r, 30));
|
|
261
|
+
assert.ok(renderJobs().includes(jid) && /job-done-ok/.test(renderJobs()), "background job runs and reports its result");
|
|
262
|
+
assert.equal((await toolByName.get("web_fetch")!.run({ url: "http://127.0.0.1/x" })).isError, true, "web_fetch blocks loopback (SSRF guard)");
|
|
263
|
+
|
|
264
|
+
// --- destructive classifier: real dangers flagged; everyday redirects are not (2>/dev/null bug) ---
|
|
265
|
+
// The /dev/ sink allow-list is boundary-anchored, so device writes whose name starts with a sink
|
|
266
|
+
// token (ttyS0, tty1) are still caught — they were a confirmed bypass before the fix.
|
|
267
|
+
for (const c of ["rm -rf /", "dd if=/dev/zero of=/dev/sda", "git push --force origin main", "git reset --hard", "> /dev/sda", "> /dev/ttyS0", "echo x > /dev/tty1"]) {
|
|
268
|
+
assert.ok(isDestructive(c), `should be destructive: ${c}`);
|
|
269
|
+
}
|
|
270
|
+
for (const c of ['ls "/some/dir" 2>/dev/null', "cat x >/dev/null", "echo hi > /dev/stdout", "grep foo bar 2> /dev/null", "node app.js &>/dev/null", "x >/dev/null 2>&1", "cat >/dev/tty"]) {
|
|
271
|
+
assert.ok(!isDestructive(c), `should NOT be destructive: ${c}`);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// --- leaked tool-call recovery (Ollama-over-stream emits the call as text) ---
|
|
275
|
+
const leaked = parseTextToolCalls('{"name": "update_todos", "arguments": {"todos": []}}');
|
|
276
|
+
assert.equal(leaked?.[0]?.name, "update_todos", "plain JSON tool call recovered");
|
|
277
|
+
const tagged = parseTextToolCalls('<tool_call>{"name":"ls","arguments":{"path":"."}}</tool_call>');
|
|
278
|
+
assert.equal(tagged?.[0]?.name, "ls", "<tool_call> wrapped call recovered");
|
|
279
|
+
assert.equal(parseTextToolCalls('{"name":"spend_time","arguments":{}}'), null, "unknown tool not treated as a call");
|
|
280
|
+
assert.equal(parseTextToolCalls("just some prose"), null, "prose is not a tool call");
|
|
281
|
+
|
|
282
|
+
// --- TUI user bar fills the full width (no void, single styled echo) ---
|
|
283
|
+
const bar = userBar("hi", 40);
|
|
284
|
+
assert.ok(bar.includes("hi") && bar.includes("›"), "user bar shows the text + marker");
|
|
285
|
+
assert.ok(bar.includes("\x1b[48;5;238m"), "user bar has a full-width background");
|
|
286
|
+
assert.ok(userBar("x".repeat(200), 40).length > 40, "over-long input does not crash padding");
|
|
287
|
+
|
|
288
|
+
// --- bundled skills load + scalable discovery (list_skills / slim use_skill) ---
|
|
289
|
+
const allSkills = loadSkills(true);
|
|
290
|
+
const skillNames = allSkills.map((s) => s.name);
|
|
291
|
+
assert.ok(skillNames.length >= 200, `>=200 skills load (got ${skillNames.length})`);
|
|
292
|
+
for (const want of ["commit", "ponytail", "dockerize", "migration", "react-hooks", "terraform-module", "pixel-diff", "canvas-debug", "connect-github", "design-system"]) {
|
|
293
|
+
assert.ok(skillNames.includes(want), `bundled skill present: ${want}`);
|
|
294
|
+
}
|
|
295
|
+
registerSkillTool(allSkills);
|
|
296
|
+
const useSkill = toolByName.get("use_skill")!;
|
|
297
|
+
assert.ok(useSkill.description.length < 400, `use_skill description is slim (got ${useSkill.description.length})`);
|
|
298
|
+
const listSkills = toolByName.get("list_skills")!;
|
|
299
|
+
const filtered = (await listSkills.run({ filter: "docker" })).output;
|
|
300
|
+
assert.ok(/dockerize/.test(filtered) && !/migration/.test(filtered), "list_skills filter narrows results");
|
|
301
|
+
assert.ok(/categories/.test((await listSkills.run({})).output), "list_skills overview lists categories");
|
|
302
|
+
|
|
303
|
+
// --- skill routing (lexical relevance ranker behind find_skill + auto-suggest) ---
|
|
304
|
+
assert.ok(rankSkills("write a database migration", allSkills, 5).some((r) => r.name === "migration"), "routing surfaces migration");
|
|
305
|
+
assert.ok(rankSkills("set up a dark mode theme", allSkills, 5).some((r) => r.name === "dark-mode"), "routing surfaces dark-mode");
|
|
306
|
+
const dockerTop = rankSkills("build a docker image for the app", allSkills, 5).map((r) => r.name);
|
|
307
|
+
assert.ok(dockerTop.includes("dockerize") || dockerTop.includes("docker-compose"), `routing surfaces a docker skill (got ${dockerTop.join(",")})`);
|
|
308
|
+
assert.equal(rankSkills("", allSkills).length, 0, "empty query → no matches");
|
|
309
|
+
|
|
310
|
+
// --- confident skill orchestration: auto-apply only on a dominant, name-exact match ---
|
|
311
|
+
assert.equal(confidentSkill("describe the project", allSkills), "project-overview", "confident: describe the project → project-overview");
|
|
312
|
+
assert.equal(confidentSkill("draw an architecture diagram of this project", allSkills), "architecture-diagram", "confident: → architecture-diagram");
|
|
313
|
+
assert.equal(confidentSkill("make a powerpoint about Q3 results", allSkills), null, "precision guard: 'powerpoint' must NOT auto-apply 'low-power'");
|
|
314
|
+
assert.equal(confidentSkill("what is 2 + 2", allSkills), null, "ambiguous query → no auto-apply");
|
|
315
|
+
// LOADED was set by registerSkillTool(allSkills) above, so routeConfident/skillBody resolve a body.
|
|
316
|
+
const applied = routeConfident("describe the project");
|
|
317
|
+
assert.ok(applied?.name === "project-overview" && /purpose/i.test(applied.body), "routeConfident returns the skill body to inject");
|
|
318
|
+
assert.equal(routeConfident("make a powerpoint about Q3 results"), null, "routeConfident respects the precision guard");
|
|
319
|
+
|
|
320
|
+
// --- connector catalog (read-only; does not touch .ada/mcp.json) ---
|
|
321
|
+
const catalog = listConnectors();
|
|
322
|
+
assert.ok(catalog.length >= 8 && catalog.some((c) => c.name === "github"), "connector catalog populated");
|
|
323
|
+
assert.ok(catalog.find((c) => c.name === "github")?.needsEnv.includes("GITHUB_PERSONAL_ACCESS_TOKEN"), "github connector declares its env var");
|
|
324
|
+
|
|
325
|
+
// --- toolsmith path end-to-end via a real stub MCP server (skips if a real .ada/mcp.json exists) ---
|
|
326
|
+
const adaDir = join(process.cwd(), ".ada");
|
|
327
|
+
const mcpCfg = join(adaDir, "mcp.json");
|
|
328
|
+
if (!existsSync(mcpCfg) && existsSync(join(process.cwd(), "test", "stub-mcp.mjs"))) {
|
|
329
|
+
mkdirSync(adaDir, { recursive: true });
|
|
330
|
+
writeFileSync(mcpCfg, JSON.stringify({ servers: { stub: { command: "node", args: ["test/stub-mcp.mjs"] } } }));
|
|
331
|
+
try {
|
|
332
|
+
const loaded = await loadMcpServers(true);
|
|
333
|
+
assert.ok(loaded.some((l) => l.startsWith("stub")), "stub MCP server connected + tools registered");
|
|
334
|
+
assert.deepEqual(configuredServers(), ["stub"], "configuredServers sees the stub");
|
|
335
|
+
assert.equal(soleIntegration(), "stub", "soleIntegration → stub");
|
|
336
|
+
const docs = readIntegrationDocs("stub");
|
|
337
|
+
assert.ok(/stub__echo/.test(docs) && /stub__add/.test(docs), "readDocs lists the stub's tools");
|
|
338
|
+
const n = writeProjectSkills([
|
|
339
|
+
{ name: "stub-echo", content: "---\nname: stub-echo\ndescription: echo via the stub\ncategory: integration-stub\n---\n# Echo\n1. call stub__echo\n## Rules\n- keep it short" },
|
|
340
|
+
{ name: "stub-junk", content: "not a skill file" },
|
|
341
|
+
]);
|
|
342
|
+
assert.equal(n, 1, "writeProjectSkills writes valid skills and skips junk");
|
|
343
|
+
assert.ok(existsSync(join(adaDir, "skills", "stub-echo", "SKILL.md")), "stub-echo SKILL.md written");
|
|
344
|
+
} finally {
|
|
345
|
+
rmSync(mcpCfg, { force: true });
|
|
346
|
+
rmSync(join(adaDir, "skills", "stub-echo"), { recursive: true, force: true });
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// --- login allowlist ---
|
|
351
|
+
assert.ok(isAllowed("anyone"), "no allowlist → allow any authenticated user");
|
|
352
|
+
process.env.ADA_ALLOWED_USERS = "alice, bob";
|
|
353
|
+
assert.ok(isAllowed("alice"));
|
|
354
|
+
assert.ok(!isAllowed("carol"), "off-allowlist user rejected");
|
|
355
|
+
delete process.env.ADA_ALLOWED_USERS;
|
|
356
|
+
|
|
357
|
+
console.log("selfcheck OK");
|
|
358
|
+
process.exit(0); // a spawned stub MCP subprocess can hold stdin open — exit cleanly
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
main().catch((e) => {
|
|
362
|
+
console.error("selfcheck FAILED:", e instanceof Error ? e.message : e);
|
|
363
|
+
process.exit(1);
|
|
364
|
+
});
|