ai-runtime-engine 1.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/CHANGELOG.md +78 -0
- package/LICENSE +15 -0
- package/README.md +489 -0
- package/dist/artifacts/artifacts.d.ts +28 -0
- package/dist/artifacts/artifacts.js +46 -0
- package/dist/benchmark/benchmark.d.ts +23 -0
- package/dist/benchmark/benchmark.js +40 -0
- package/dist/cli/cli.d.ts +6 -0
- package/dist/cli/cli.js +161 -0
- package/dist/cli/commands/cleanup.d.ts +47 -0
- package/dist/cli/commands/cleanup.js +96 -0
- package/dist/cli/commands/config.d.ts +8 -0
- package/dist/cli/commands/config.js +28 -0
- package/dist/cli/commands/doctor.d.ts +57 -0
- package/dist/cli/commands/doctor.js +86 -0
- package/dist/cli/commands/executions.d.ts +9 -0
- package/dist/cli/commands/executions.js +25 -0
- package/dist/cli/commands/info.d.ts +43 -0
- package/dist/cli/commands/info.js +53 -0
- package/dist/cli/commands/init.d.ts +5 -0
- package/dist/cli/commands/init.js +75 -0
- package/dist/cli/commands/inspect.d.ts +16 -0
- package/dist/cli/commands/inspect.js +60 -0
- package/dist/cli/commands/phase2.d.ts +22 -0
- package/dist/cli/commands/phase2.js +83 -0
- package/dist/cli/commands/route.d.ts +14 -0
- package/dist/cli/commands/route.js +49 -0
- package/dist/cli/commands/run.d.ts +11 -0
- package/dist/cli/commands/run.js +37 -0
- package/dist/cli/commands/setup.d.ts +34 -0
- package/dist/cli/commands/setup.js +104 -0
- package/dist/cli/commands/skills.d.ts +28 -0
- package/dist/cli/commands/skills.js +48 -0
- package/dist/cli/commands/test.d.ts +7 -0
- package/dist/cli/commands/test.js +29 -0
- package/dist/cli/context.d.ts +12 -0
- package/dist/cli/context.js +16 -0
- package/dist/cli/interactive/repl.d.ts +6 -0
- package/dist/cli/interactive/repl.js +45 -0
- package/dist/cli/interactive/session.d.ts +36 -0
- package/dist/cli/interactive/session.js +356 -0
- package/dist/cli/prompt.d.ts +6 -0
- package/dist/cli/prompt.js +18 -0
- package/dist/cli/render.d.ts +7 -0
- package/dist/cli/render.js +14 -0
- package/dist/comparison/analysis.d.ts +46 -0
- package/dist/comparison/analysis.js +177 -0
- package/dist/comparison/comparator.d.ts +46 -0
- package/dist/comparison/comparator.js +270 -0
- package/dist/comparison/comparison.d.ts +140 -0
- package/dist/comparison/comparison.js +9 -0
- package/dist/comparison/render.d.ts +7 -0
- package/dist/comparison/render.js +66 -0
- package/dist/config/defaults.d.ts +52 -0
- package/dist/config/defaults.js +56 -0
- package/dist/config/load.d.ts +17 -0
- package/dist/config/load.js +50 -0
- package/dist/config/providerDefaults.d.ts +17 -0
- package/dist/config/providerDefaults.js +61 -0
- package/dist/config/schema.d.ts +9 -0
- package/dist/config/schema.js +78 -0
- package/dist/context/budget.d.ts +13 -0
- package/dist/context/budget.js +17 -0
- package/dist/context/compiler.d.ts +61 -0
- package/dist/context/compiler.js +125 -0
- package/dist/context/tokens.d.ts +19 -0
- package/dist/context/tokens.js +38 -0
- package/dist/conversations/conversations.d.ts +38 -0
- package/dist/conversations/conversations.js +64 -0
- package/dist/core/capabilities/evidence.d.ts +40 -0
- package/dist/core/capabilities/evidence.js +102 -0
- package/dist/core/capabilities/overlay.d.ts +15 -0
- package/dist/core/capabilities/overlay.js +0 -0
- package/dist/core/capabilities/taxonomy.d.ts +19 -0
- package/dist/core/capabilities/taxonomy.js +25 -0
- package/dist/core/fallback/errors.d.ts +30 -0
- package/dist/core/fallback/errors.js +80 -0
- package/dist/core/fallback/fallback.d.ts +40 -0
- package/dist/core/fallback/fallback.js +82 -0
- package/dist/core/fallback/retryPolicy.d.ts +11 -0
- package/dist/core/fallback/retryPolicy.js +14 -0
- package/dist/core/health/health.d.ts +3 -0
- package/dist/core/health/health.js +5 -0
- package/dist/core/health/monitor.d.ts +23 -0
- package/dist/core/health/monitor.js +82 -0
- package/dist/core/policies/budget.d.ts +19 -0
- package/dist/core/policies/budget.js +37 -0
- package/dist/core/registry/builtinTasks.d.ts +8 -0
- package/dist/core/registry/builtinTasks.js +54 -0
- package/dist/core/registry/registry.d.ts +18 -0
- package/dist/core/registry/registry.js +33 -0
- package/dist/core/registry/taskRegistry.d.ts +15 -0
- package/dist/core/registry/taskRegistry.js +30 -0
- package/dist/core/router/confidence.d.ts +7 -0
- package/dist/core/router/confidence.js +20 -0
- package/dist/core/router/dimensions.d.ts +16 -0
- package/dist/core/router/dimensions.js +60 -0
- package/dist/core/router/executor.d.ts +16 -0
- package/dist/core/router/executor.js +25 -0
- package/dist/core/router/filter.d.ts +34 -0
- package/dist/core/router/filter.js +113 -0
- package/dist/core/router/normalize.d.ts +30 -0
- package/dist/core/router/normalize.js +119 -0
- package/dist/core/router/request.d.ts +4 -0
- package/dist/core/router/request.js +21 -0
- package/dist/core/router/router.d.ts +32 -0
- package/dist/core/router/router.js +195 -0
- package/dist/core/router/routingPrefs.d.ts +11 -0
- package/dist/core/router/routingPrefs.js +30 -0
- package/dist/core/router/scorer.d.ts +19 -0
- package/dist/core/router/scorer.js +50 -0
- package/dist/core/router/weights.d.ts +9 -0
- package/dist/core/router/weights.js +31 -0
- package/dist/core/validation/validator.d.ts +16 -0
- package/dist/core/validation/validator.js +33 -0
- package/dist/discovery/modelCatalog.d.ts +28 -0
- package/dist/discovery/modelCatalog.js +105 -0
- package/dist/discovery/openapi.d.ts +25 -0
- package/dist/discovery/openapi.js +76 -0
- package/dist/executions/checkpoint.d.ts +26 -0
- package/dist/executions/checkpoint.js +114 -0
- package/dist/executions/execution.d.ts +51 -0
- package/dist/executions/execution.js +8 -0
- package/dist/executions/store.d.ts +52 -0
- package/dist/executions/store.js +124 -0
- package/dist/generation/generateAdapter.d.ts +17 -0
- package/dist/generation/generateAdapter.js +30 -0
- package/dist/index.d.ts +147 -0
- package/dist/index.js +107 -0
- package/dist/learning/feedback.d.ts +9 -0
- package/dist/learning/feedback.js +18 -0
- package/dist/learning/learningStore.d.ts +68 -0
- package/dist/learning/learningStore.js +138 -0
- package/dist/learning/performanceStore.d.ts +27 -0
- package/dist/learning/performanceStore.js +0 -0
- package/dist/marketplace/presets.d.ts +24 -0
- package/dist/marketplace/presets.js +52 -0
- package/dist/mcp/mcp.d.ts +31 -0
- package/dist/mcp/mcp.js +54 -0
- package/dist/memory/bm25.d.ts +16 -0
- package/dist/memory/bm25.js +56 -0
- package/dist/memory/classifier.d.ts +14 -0
- package/dist/memory/classifier.js +17 -0
- package/dist/memory/memory.d.ts +80 -0
- package/dist/memory/memory.js +191 -0
- package/dist/orchestration/executor.d.ts +35 -0
- package/dist/orchestration/executor.js +65 -0
- package/dist/orchestration/orchestrator.d.ts +42 -0
- package/dist/orchestration/orchestrator.js +63 -0
- package/dist/orchestration/plan.d.ts +37 -0
- package/dist/orchestration/plan.js +70 -0
- package/dist/orchestration/planner.d.ts +29 -0
- package/dist/orchestration/planner.js +69 -0
- package/dist/plugin/ai.d.ts +82 -0
- package/dist/plugin/ai.js +167 -0
- package/dist/probing/probe.d.ts +25 -0
- package/dist/probing/probe.js +63 -0
- package/dist/providers/factory.d.ts +18 -0
- package/dist/providers/factory.js +54 -0
- package/dist/providers/httpClient.d.ts +34 -0
- package/dist/providers/httpClient.js +80 -0
- package/dist/providers/httpProvider.d.ts +49 -0
- package/dist/providers/httpProvider.js +135 -0
- package/dist/providers/mock/demo.d.ts +13 -0
- package/dist/providers/mock/demo.js +58 -0
- package/dist/providers/mock/mockProvider.d.ts +35 -0
- package/dist/providers/mock/mockProvider.js +121 -0
- package/dist/providers/mock/scenarios.d.ts +44 -0
- package/dist/providers/mock/scenarios.js +30 -0
- package/dist/providers/provider.d.ts +26 -0
- package/dist/providers/provider.js +11 -0
- package/dist/providers/wire/anthropicWire.d.ts +6 -0
- package/dist/providers/wire/anthropicWire.js +83 -0
- package/dist/providers/wire/openaiWire.d.ts +7 -0
- package/dist/providers/wire/openaiWire.js +81 -0
- package/dist/providers/wire/registry.d.ts +8 -0
- package/dist/providers/wire/registry.js +20 -0
- package/dist/providers/wire/types.d.ts +39 -0
- package/dist/providers/wire/types.js +24 -0
- package/dist/runtime/config.d.ts +31 -0
- package/dist/runtime/config.js +121 -0
- package/dist/runtime/context.d.ts +34 -0
- package/dist/runtime/context.js +11 -0
- package/dist/runtime/events.d.ts +99 -0
- package/dist/runtime/events.js +82 -0
- package/dist/runtime/host.d.ts +27 -0
- package/dist/runtime/host.js +7 -0
- package/dist/runtime/intent/classifier.d.ts +30 -0
- package/dist/runtime/intent/classifier.js +60 -0
- package/dist/runtime/intent/signals.d.ts +19 -0
- package/dist/runtime/intent/signals.js +46 -0
- package/dist/runtime/modes/availability.d.ts +11 -0
- package/dist/runtime/modes/availability.js +17 -0
- package/dist/runtime/modes/chat.d.ts +18 -0
- package/dist/runtime/modes/chat.js +67 -0
- package/dist/runtime/modes/modeResolver.d.ts +43 -0
- package/dist/runtime/modes/modeResolver.js +78 -0
- package/dist/runtime/policy.d.ts +72 -0
- package/dist/runtime/policy.js +59 -0
- package/dist/runtime/providerView.d.ts +62 -0
- package/dist/runtime/providerView.js +105 -0
- package/dist/runtime/routing.d.ts +26 -0
- package/dist/runtime/routing.js +65 -0
- package/dist/runtime/runtime.d.ts +191 -0
- package/dist/runtime/runtime.js +718 -0
- package/dist/runtime/types.d.ts +153 -0
- package/dist/runtime/types.js +9 -0
- package/dist/runtime/workspace/detectors.d.ts +15 -0
- package/dist/runtime/workspace/detectors.js +57 -0
- package/dist/runtime/workspace/workspace.d.ts +29 -0
- package/dist/runtime/workspace/workspace.js +116 -0
- package/dist/security/credentials.d.ts +26 -0
- package/dist/security/credentials.js +34 -0
- package/dist/security/redact.d.ts +16 -0
- package/dist/security/redact.js +57 -0
- package/dist/skills/builtins/fileAnalyzer.d.ts +7 -0
- package/dist/skills/builtins/fileAnalyzer.js +47 -0
- package/dist/skills/builtins/repositoryAnalyzer.d.ts +6 -0
- package/dist/skills/builtins/repositoryAnalyzer.js +47 -0
- package/dist/skills/discovery.d.ts +61 -0
- package/dist/skills/discovery.js +211 -0
- package/dist/skills/manifest.d.ts +30 -0
- package/dist/skills/manifest.js +75 -0
- package/dist/skills/registry.d.ts +15 -0
- package/dist/skills/registry.js +22 -0
- package/dist/skills/skill.d.ts +64 -0
- package/dist/skills/skill.js +8 -0
- package/dist/store/area.d.ts +54 -0
- package/dist/store/area.js +164 -0
- package/dist/store/paths.d.ts +15 -0
- package/dist/store/paths.js +48 -0
- package/dist/store/store.d.ts +59 -0
- package/dist/store/store.js +140 -0
- package/dist/telemetry/sinks/file.d.ts +12 -0
- package/dist/telemetry/sinks/file.js +28 -0
- package/dist/telemetry/telemetry.d.ts +36 -0
- package/dist/telemetry/telemetry.js +63 -0
- package/dist/tools/builtins/filesystem.d.ts +7 -0
- package/dist/tools/builtins/filesystem.js +53 -0
- package/dist/tools/builtins/git.d.ts +10 -0
- package/dist/tools/builtins/git.js +66 -0
- package/dist/tools/builtins/shell.d.ts +17 -0
- package/dist/tools/builtins/shell.js +91 -0
- package/dist/tools/jail.d.ts +12 -0
- package/dist/tools/jail.js +98 -0
- package/dist/tools/permissions.d.ts +25 -0
- package/dist/tools/permissions.js +24 -0
- package/dist/tools/registry.d.ts +10 -0
- package/dist/tools/registry.js +20 -0
- package/dist/tools/runner.d.ts +23 -0
- package/dist/tools/runner.js +64 -0
- package/dist/tools/tool.d.ts +53 -0
- package/dist/tools/tool.js +24 -0
- package/dist/tools/untrusted.d.ts +13 -0
- package/dist/tools/untrusted.js +30 -0
- package/dist/types.d.ts +460 -0
- package/dist/types.js +12 -0
- package/dist/util/clock.d.ts +6 -0
- package/dist/util/clock.js +4 -0
- package/dist/util/extractJson.d.ts +8 -0
- package/dist/util/extractJson.js +54 -0
- package/dist/verification/verify.d.ts +26 -0
- package/dist/verification/verify.js +67 -0
- package/docs/GUIDE.md +358 -0
- package/docs/README.md +21 -0
- package/docs/architecture.md +78 -0
- package/docs/router.md +376 -0
- package/docs/security.md +55 -0
- package/package.json +67 -0
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `ai-runtime info` — a read-only summary of the runtime: workspace, default mode, context budget,
|
|
3
|
+
* providers/models, skills, tools, memory + learning stats, and storage paths. Makes NO live calls and
|
|
4
|
+
* mutates nothing. `--json` prints the structured report.
|
|
5
|
+
*/
|
|
6
|
+
import { Runtime } from '../../runtime/runtime.js';
|
|
7
|
+
import { resolveContextBudget } from '../../context/budget.js';
|
|
8
|
+
import { summarizeWorkspace } from '../../runtime/workspace/workspace.js';
|
|
9
|
+
import { print } from '../render.js';
|
|
10
|
+
/** Gather the read-only info report from a loaded runtime. Pure aside from reading the local store. */
|
|
11
|
+
export async function gatherInfo(rt) {
|
|
12
|
+
const ws = rt.workspaceInfo();
|
|
13
|
+
const settings = rt.settings();
|
|
14
|
+
const env = process.env;
|
|
15
|
+
const budget = resolveContextBudget({
|
|
16
|
+
...(settings.context?.maxTokens !== undefined ? { config: settings.context.maxTokens } : {}),
|
|
17
|
+
...(env.AI_CONTEXT_MAX_TOKENS !== undefined ? { env: env.AI_CONTEXT_MAX_TOKENS } : {}),
|
|
18
|
+
});
|
|
19
|
+
const providers = rt.ai.providers();
|
|
20
|
+
const models = await rt.ai.discover().then((m) => m.reduce((n, p) => n + p.models.length, 0)).catch(() => 0);
|
|
21
|
+
const facts = rt.memory.enabled ? rt.memory.all().filter((r) => !r.supersededBy).length : 0;
|
|
22
|
+
const learningSnap = rt.learning.enabled ? rt.learning.snapshot() : [];
|
|
23
|
+
const paths = rt.store.paths();
|
|
24
|
+
return {
|
|
25
|
+
...(ws ? { workspace: { name: ws.name ?? ws.root.split(/[\\/]/).pop() ?? 'workspace', summary: summarizeWorkspace(ws), ...(ws.git.branch ? { branch: ws.git.branch } : {}) } } : {}),
|
|
26
|
+
defaultMode: settings.defaultMode ?? 'auto',
|
|
27
|
+
contextBudgetTokens: budget,
|
|
28
|
+
providers: { total: providers.length, enabled: providers.filter((p) => p.enabled).length },
|
|
29
|
+
models,
|
|
30
|
+
skills: rt.skills().map((s) => `${s.id}@${s.version}`),
|
|
31
|
+
tools: rt.tools().map((t) => t.id),
|
|
32
|
+
memory: { enabled: rt.memory.enabled, facts },
|
|
33
|
+
learning: { enabled: rt.learning.enabled, entries: learningSnap.length, preferredProviders: rt.learning.enabled ? rt.learning.preferredProviders() : [] },
|
|
34
|
+
storage: { home: paths.home, project: paths.project, enabled: rt.store.enabled },
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
export function renderInfo(r) {
|
|
38
|
+
const lines = ['ai-runtime info', ''];
|
|
39
|
+
if (r.workspace)
|
|
40
|
+
lines.push(`workspace: ${r.workspace.name}${r.workspace.branch ? ` @ ${r.workspace.branch}` : ''}`, ` ${r.workspace.summary}`);
|
|
41
|
+
else
|
|
42
|
+
lines.push('workspace: (none detected)');
|
|
43
|
+
lines.push(`mode: ${r.defaultMode}`, `context: ${r.contextBudgetTokens} token budget`, `providers: ${r.providers.total} configured (${r.providers.enabled} enabled), ${r.models} models`, `skills: ${r.skills.length ? r.skills.join(', ') : '(none — register tools to enable)'}`, `tools: ${r.tools.join(', ')}`, `memory: ${r.memory.enabled ? `${r.memory.facts} fact(s)` : 'disabled'}`, `learning: ${r.learning.enabled ? `${r.learning.entries} record(s)${r.learning.preferredProviders.length ? `, prefers ${r.learning.preferredProviders.join(', ')}` : ''}` : 'disabled'}`, `storage: ${r.storage.enabled ? r.storage.project : 'disabled (stateless)'}`);
|
|
44
|
+
return lines;
|
|
45
|
+
}
|
|
46
|
+
export async function infoCommand(opts) {
|
|
47
|
+
const rt = await Runtime.load({ ...(opts.config ? { config: opts.config } : {}) });
|
|
48
|
+
const report = await gatherInfo(rt);
|
|
49
|
+
if (opts.json)
|
|
50
|
+
return print(JSON.stringify(report, null, 2));
|
|
51
|
+
for (const line of renderInfo(report))
|
|
52
|
+
print(line);
|
|
53
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `ai-runtime init` — scaffold a config, an env template, and gitignore entries. Never writes secrets;
|
|
3
|
+
* the config carries only env-var NAMES, and `.env` is added to .gitignore.
|
|
4
|
+
*/
|
|
5
|
+
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
6
|
+
import { resolve } from 'node:path';
|
|
7
|
+
import { print } from '../render.js';
|
|
8
|
+
const EXAMPLE_YAML = `# ai-runtime configuration. Secret VALUES live in .env (gitignored); this file names env vars only.
|
|
9
|
+
strategy: best
|
|
10
|
+
|
|
11
|
+
providers:
|
|
12
|
+
- id: gemini
|
|
13
|
+
kind: gemini
|
|
14
|
+
apiKeyEnv: GEMINI_API_KEY
|
|
15
|
+
|
|
16
|
+
- id: groq
|
|
17
|
+
kind: groq
|
|
18
|
+
apiKeyEnv: GROQ_API_KEY
|
|
19
|
+
|
|
20
|
+
- id: anthropic
|
|
21
|
+
kind: anthropic
|
|
22
|
+
apiKeyEnv: ANTHROPIC_API_KEY
|
|
23
|
+
|
|
24
|
+
# Local — no key required. Remove if you do not run Ollama.
|
|
25
|
+
- id: ollama
|
|
26
|
+
kind: ollama
|
|
27
|
+
|
|
28
|
+
# Example of an unknown OpenAI-compatible provider that self-declares its capabilities:
|
|
29
|
+
# - id: internal
|
|
30
|
+
# kind: openai-compatible
|
|
31
|
+
# baseUrl: https://ai.internal.example/v1
|
|
32
|
+
# apiKeyEnv: INTERNAL_AI_API_KEY
|
|
33
|
+
# models: [internal-large]
|
|
34
|
+
# capabilities:
|
|
35
|
+
# intelligence: [reasoning, code_generation]
|
|
36
|
+
# output: [text, code, json]
|
|
37
|
+
# contextWindow: 128000
|
|
38
|
+
`;
|
|
39
|
+
const EXAMPLE_ENV = `# Fill in the values you have. Leave the rest blank — the router adapts to whatever is configured.
|
|
40
|
+
GEMINI_API_KEY=
|
|
41
|
+
GROQ_API_KEY=
|
|
42
|
+
ANTHROPIC_API_KEY=
|
|
43
|
+
`;
|
|
44
|
+
const GITIGNORE_ENTRIES = ['.env', '.env.*', '!.env.example', '.ai-runtime/'];
|
|
45
|
+
function writeIfAbsent(file, contents) {
|
|
46
|
+
const full = resolve(process.cwd(), file);
|
|
47
|
+
if (existsSync(full)) {
|
|
48
|
+
print(` • ${file} already exists — left unchanged`);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
writeFileSync(full, contents);
|
|
52
|
+
print(` ✓ wrote ${file}`);
|
|
53
|
+
}
|
|
54
|
+
function ensureGitignore() {
|
|
55
|
+
const full = resolve(process.cwd(), '.gitignore');
|
|
56
|
+
const existing = existsSync(full) ? readFileSync(full, 'utf8') : '';
|
|
57
|
+
const missing = GITIGNORE_ENTRIES.filter((e) => !existing.split('\n').includes(e));
|
|
58
|
+
if (missing.length === 0) {
|
|
59
|
+
print(' • .gitignore already covers secrets');
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
const block = `${existing && !existing.endsWith('\n') ? '\n' : ''}\n# ai-runtime secrets\n${missing.join('\n')}\n`;
|
|
63
|
+
writeFileSync(full, existing + block);
|
|
64
|
+
print(` ✓ added ${missing.length} entr${missing.length === 1 ? 'y' : 'ies'} to .gitignore`);
|
|
65
|
+
}
|
|
66
|
+
export function initCommand() {
|
|
67
|
+
print('Initializing ai-runtime...');
|
|
68
|
+
writeIfAbsent('ai-runtime.yaml', EXAMPLE_YAML);
|
|
69
|
+
writeIfAbsent('.env.example', EXAMPLE_ENV);
|
|
70
|
+
ensureGitignore();
|
|
71
|
+
print('\nNext steps:');
|
|
72
|
+
print(' 1. Copy .env.example to .env and fill in the keys you have');
|
|
73
|
+
print(' 2. Run `ai-runtime doctor` to verify configuration');
|
|
74
|
+
print(' 3. Run `ai-runtime route <task>` to route a task');
|
|
75
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `ai-runtime providers` / `models` / `capabilities` — read-only inspection of the configured fleet.
|
|
3
|
+
* All go through the key-free facade views and redacted output.
|
|
4
|
+
*/
|
|
5
|
+
export declare function providersCommand(opts: {
|
|
6
|
+
config?: string;
|
|
7
|
+
json?: boolean;
|
|
8
|
+
}): Promise<void>;
|
|
9
|
+
export declare function modelsCommand(opts: {
|
|
10
|
+
config?: string;
|
|
11
|
+
json?: boolean;
|
|
12
|
+
}): Promise<void>;
|
|
13
|
+
export declare function capabilitiesCommand(opts: {
|
|
14
|
+
config?: string;
|
|
15
|
+
json?: boolean;
|
|
16
|
+
}): Promise<void>;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `ai-runtime providers` / `models` / `capabilities` — read-only inspection of the configured fleet.
|
|
3
|
+
* All go through the key-free facade views and redacted output.
|
|
4
|
+
*/
|
|
5
|
+
import { loadAI } from '../context.js';
|
|
6
|
+
import { print } from '../render.js';
|
|
7
|
+
export async function providersCommand(opts) {
|
|
8
|
+
const { ai, hasProviders } = loadAI(opts.config);
|
|
9
|
+
if (!hasProviders)
|
|
10
|
+
return print('No providers configured.');
|
|
11
|
+
const health = await ai.checkHealth();
|
|
12
|
+
const healthById = new Map(health.map((h) => [h.providerId, h]));
|
|
13
|
+
const list = ai.providers();
|
|
14
|
+
if (opts.json)
|
|
15
|
+
return print(JSON.stringify(list, null, 2));
|
|
16
|
+
print('Providers:');
|
|
17
|
+
for (const p of list) {
|
|
18
|
+
const h = healthById.get(p.id);
|
|
19
|
+
print(` ${p.enabled ? '●' : '○'} ${p.id.padEnd(14)} ${p.kind.padEnd(18)} ${p.privacyClass.padEnd(6)} ${h?.state ?? 'UNKNOWN'}`);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
export async function modelsCommand(opts) {
|
|
23
|
+
const { ai, hasProviders } = loadAI(opts.config);
|
|
24
|
+
if (!hasProviders)
|
|
25
|
+
return print('No providers configured.');
|
|
26
|
+
const metas = await ai.discover();
|
|
27
|
+
if (opts.json)
|
|
28
|
+
return print(JSON.stringify(metas.flatMap((m) => m.models), null, 2));
|
|
29
|
+
print('Models:');
|
|
30
|
+
for (const meta of metas) {
|
|
31
|
+
for (const model of meta.models) {
|
|
32
|
+
const ctx = model.contextWindow ? `${Math.round(model.contextWindow / 1000)}k` : '?';
|
|
33
|
+
const tier = model.quality?.tier ?? '—';
|
|
34
|
+
print(` ${meta.id.padEnd(12)} ${model.id.padEnd(24)} ctx=${ctx.padEnd(6)} quality=${tier.padEnd(9)} source=${model.source}`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
const GROUPS = ['input', 'output', 'intelligence', 'agent'];
|
|
39
|
+
export async function capabilitiesCommand(opts) {
|
|
40
|
+
const { ai, hasProviders } = loadAI(opts.config);
|
|
41
|
+
if (!hasProviders)
|
|
42
|
+
return print('No providers configured.');
|
|
43
|
+
const metas = await ai.discover();
|
|
44
|
+
if (opts.json)
|
|
45
|
+
return print(JSON.stringify(metas.flatMap((m) => m.models.map((mo) => ({ provider: m.id, model: mo.id, capabilities: mo.capabilities }))), null, 2));
|
|
46
|
+
print('Capabilities (supported keys, by model):');
|
|
47
|
+
for (const meta of metas) {
|
|
48
|
+
for (const model of meta.models) {
|
|
49
|
+
print(`\n ${meta.id}/${model.id}`);
|
|
50
|
+
for (const group of GROUPS) {
|
|
51
|
+
const bucket = model.capabilities[group];
|
|
52
|
+
const keys = Object.entries(bucket).filter(([, cap]) => cap.value).map(([k, cap]) => `${k}(${cap.evidence[0]})`);
|
|
53
|
+
if (keys.length)
|
|
54
|
+
print(` ${group.padEnd(13)} ${keys.join(', ')}`);
|
|
55
|
+
}
|
|
56
|
+
const ctx = model.capabilities.context.contextWindow;
|
|
57
|
+
print(` context ${ctx.value} (${ctx.evidence})`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Phase-2 CLI commands: probe (verify capabilities), discover (analyze an OpenAPI spec),
|
|
3
|
+
* benchmark (compare providers on representative tasks), telemetry (recent routing events).
|
|
4
|
+
*/
|
|
5
|
+
export declare function probeCommand(providerId: string, model: string, opts: {
|
|
6
|
+
config?: string;
|
|
7
|
+
}): Promise<void>;
|
|
8
|
+
export declare function discoverCommand(specFile: string, opts: {
|
|
9
|
+
json?: boolean;
|
|
10
|
+
}): void;
|
|
11
|
+
export declare function benchmarkCommand(opts: {
|
|
12
|
+
config?: string;
|
|
13
|
+
samples?: string;
|
|
14
|
+
json?: boolean;
|
|
15
|
+
}): Promise<void>;
|
|
16
|
+
export declare function telemetryCommand(opts: {
|
|
17
|
+
config?: string;
|
|
18
|
+
limit?: string;
|
|
19
|
+
}): void;
|
|
20
|
+
export declare function presetsCommand(opts: {
|
|
21
|
+
json?: boolean;
|
|
22
|
+
}): void;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Phase-2 CLI commands: probe (verify capabilities), discover (analyze an OpenAPI spec),
|
|
3
|
+
* benchmark (compare providers on representative tasks), telemetry (recent routing events).
|
|
4
|
+
*/
|
|
5
|
+
import { loadAI } from '../context.js';
|
|
6
|
+
import { print, printError } from '../render.js';
|
|
7
|
+
import { analyzeOpenApiFile } from '../../discovery/openapi.js';
|
|
8
|
+
import { benchmark } from '../../benchmark/benchmark.js';
|
|
9
|
+
import { listPresets } from '../../marketplace/presets.js';
|
|
10
|
+
export async function probeCommand(providerId, model, opts) {
|
|
11
|
+
const { ai, hasProviders } = loadAI(opts.config);
|
|
12
|
+
if (!hasProviders)
|
|
13
|
+
return print('No providers configured.');
|
|
14
|
+
print(`Probing ${providerId}/${model} (live calls)...`);
|
|
15
|
+
try {
|
|
16
|
+
const report = await ai.probe(providerId, model);
|
|
17
|
+
for (const r of report.results)
|
|
18
|
+
print(` ${r.status === 'verified' ? '✓' : '·'} ${r.probe.padEnd(18)} ${r.status}${r.detail ? ` (${r.detail})` : ''}`);
|
|
19
|
+
const verified = report.results.filter((r) => r.status === 'verified').length;
|
|
20
|
+
print(`\n${verified}/${report.results.length} capabilities verified and applied to the routing overlay.`);
|
|
21
|
+
}
|
|
22
|
+
catch (e) {
|
|
23
|
+
printError(`probe failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
24
|
+
process.exitCode = 1;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
export function discoverCommand(specFile, opts) {
|
|
28
|
+
const analysis = analyzeOpenApiFile(specFile);
|
|
29
|
+
if (opts.json)
|
|
30
|
+
return print(JSON.stringify(analysis, null, 2));
|
|
31
|
+
print(`OpenAPI analysis: ${analysis.title ?? specFile}${analysis.version ? ` v${analysis.version}` : ''}`);
|
|
32
|
+
print(` baseUrl: ${analysis.baseUrl ?? '(not found)'}`);
|
|
33
|
+
print(` auth: ${analysis.authMode}${analysis.authHeaderName ? ` (${analysis.authHeaderName})` : ''}`);
|
|
34
|
+
print(` wireShape: ${analysis.wireShape}`);
|
|
35
|
+
print(` chat: ${analysis.chatEndpoint ?? '(none detected)'}`);
|
|
36
|
+
print(` endpoints: ${analysis.endpoints.length}`);
|
|
37
|
+
if (analysis.suggestedProviderConfig) {
|
|
38
|
+
print('\nSuggested provider config (review before use):');
|
|
39
|
+
print(JSON.stringify(analysis.suggestedProviderConfig, null, 2).split('\n').map((l) => ` ${l}`).join('\n'));
|
|
40
|
+
}
|
|
41
|
+
print('\nNotes:');
|
|
42
|
+
for (const n of analysis.notes)
|
|
43
|
+
print(` - ${n}`);
|
|
44
|
+
}
|
|
45
|
+
const DEFAULT_BENCH_TASKS = [
|
|
46
|
+
{ task: 'generate-playwright-tests', input: 'A login form with email, password, and remember-me.' },
|
|
47
|
+
{ task: 'jira-to-test', input: 'As a user I can reset my password via an emailed link.' },
|
|
48
|
+
];
|
|
49
|
+
export async function benchmarkCommand(opts) {
|
|
50
|
+
const { ai, hasProviders } = loadAI(opts.config);
|
|
51
|
+
if (!hasProviders)
|
|
52
|
+
return print('No providers configured.');
|
|
53
|
+
const samples = opts.samples ? Number(opts.samples) : 3;
|
|
54
|
+
print(`Benchmarking ${DEFAULT_BENCH_TASKS.length} tasks × ${samples} samples per provider...\n`);
|
|
55
|
+
const results = await benchmark(ai, DEFAULT_BENCH_TASKS, { samples });
|
|
56
|
+
if (opts.json)
|
|
57
|
+
return print(JSON.stringify(results, null, 2));
|
|
58
|
+
print(`${'Provider'.padEnd(16)}${'Success'.padEnd(10)}${'AvgLatency'.padEnd(12)}Runs`);
|
|
59
|
+
for (const r of results) {
|
|
60
|
+
print(`${r.providerId.padEnd(16)}${`${Math.round(r.successRate * 100)}%`.padEnd(10)}${`${Math.round(r.avgLatencyMs)}ms`.padEnd(12)}${r.runs}`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
export function telemetryCommand(opts) {
|
|
64
|
+
const { ai } = loadAI(opts.config);
|
|
65
|
+
const events = ai.telemetryEvents();
|
|
66
|
+
const limit = opts.limit ? Number(opts.limit) : 20;
|
|
67
|
+
const recent = events.slice(-limit);
|
|
68
|
+
if (recent.length === 0)
|
|
69
|
+
return print('No telemetry recorded in this session.');
|
|
70
|
+
for (const e of recent)
|
|
71
|
+
print(JSON.stringify(e));
|
|
72
|
+
}
|
|
73
|
+
export function presetsCommand(opts) {
|
|
74
|
+
const presets = listPresets();
|
|
75
|
+
if (opts.json)
|
|
76
|
+
return print(JSON.stringify(presets, null, 2));
|
|
77
|
+
print('Provider marketplace presets:');
|
|
78
|
+
for (const p of presets) {
|
|
79
|
+
print(` ${p.id.padEnd(14)} ${p.kind.padEnd(18)} ${p.apiKeyEnv ?? '(no key)'}`);
|
|
80
|
+
print(` ${''.padEnd(14)} ${p.description}`);
|
|
81
|
+
}
|
|
82
|
+
print('\nAdd one in config with `kind` + `apiKeyEnv`, or programmatically via `ai.addPreset(id)`.');
|
|
83
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `ai-runtime route <task>` — run a task through the router and print the decision + result.
|
|
3
|
+
* Routes over the providers built from config; when none are configured it falls back to the offline
|
|
4
|
+
* demo providers (no keys). All output passes through redact().
|
|
5
|
+
*/
|
|
6
|
+
export interface RouteOptions {
|
|
7
|
+
input?: string;
|
|
8
|
+
strategy?: string;
|
|
9
|
+
provider?: string;
|
|
10
|
+
model?: string;
|
|
11
|
+
json?: boolean;
|
|
12
|
+
config?: string;
|
|
13
|
+
}
|
|
14
|
+
export declare function routeCommand(task: string, options: RouteOptions): Promise<void>;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `ai-runtime route <task>` — run a task through the router and print the decision + result.
|
|
3
|
+
* Routes over the providers built from config; when none are configured it falls back to the offline
|
|
4
|
+
* demo providers (no keys). All output passes through redact().
|
|
5
|
+
*/
|
|
6
|
+
import { buildDemoAI } from '../../providers/mock/demo.js';
|
|
7
|
+
import { loadAI } from '../context.js';
|
|
8
|
+
import { print } from '../render.js';
|
|
9
|
+
export async function routeCommand(task, options) {
|
|
10
|
+
const loaded = loadAI(options.config);
|
|
11
|
+
const ai = loaded.hasProviders ? loaded.ai : buildDemoAI();
|
|
12
|
+
if (!loaded.hasProviders)
|
|
13
|
+
print('(no providers configured — routing against offline demo providers)\n');
|
|
14
|
+
const request = { task, input: options.input ?? `demo input for ${task}` };
|
|
15
|
+
if (options.strategy)
|
|
16
|
+
request.strategy = options.strategy;
|
|
17
|
+
if (options.provider)
|
|
18
|
+
request.provider = options.provider;
|
|
19
|
+
if (options.model)
|
|
20
|
+
request.model = options.model;
|
|
21
|
+
const result = await ai.run(request);
|
|
22
|
+
if (options.json) {
|
|
23
|
+
print(JSON.stringify(result, null, 2));
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
print(`Task: ${task}`);
|
|
27
|
+
print(`Result: ${result.ok ? 'OK' : 'FAILED'} confidence: ${result.confidence.toFixed(2)}`);
|
|
28
|
+
if (result.routing.selected)
|
|
29
|
+
print(`Selected: ${result.routing.selected.providerId}/${result.routing.selected.model}`);
|
|
30
|
+
print('\nRanked candidates:');
|
|
31
|
+
for (const r of result.routing.ranked)
|
|
32
|
+
print(` ${r.score.toFixed(3)} ${r.providerId}/${r.model}`);
|
|
33
|
+
if (result.routing.filteredOut.length) {
|
|
34
|
+
print('\nExcluded:');
|
|
35
|
+
for (const f of result.routing.filteredOut)
|
|
36
|
+
print(` ${f.providerId}/${f.model}: ${f.reason}`);
|
|
37
|
+
}
|
|
38
|
+
if (result.routing.attempts.length) {
|
|
39
|
+
print('\nAttempts:');
|
|
40
|
+
for (const a of result.routing.attempts)
|
|
41
|
+
print(` ${a.providerId}/${a.model}: ${a.outcome}${a.category ? ` (${a.category})` : ''} ${a.latencyMs}ms`);
|
|
42
|
+
}
|
|
43
|
+
if (!result.ok && result.error)
|
|
44
|
+
print(`\nError [${result.error.category}]: ${result.error.message}`);
|
|
45
|
+
else if (result.response?.text)
|
|
46
|
+
print(`\nResponse: ${result.response.text}`);
|
|
47
|
+
else if (result.response?.json)
|
|
48
|
+
print(`\nResponse (json): ${JSON.stringify(result.response.json)}`);
|
|
49
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `ai-runtime run "<input>"` — one-shot Runtime invocation for CI/scripts. Shares the exact Runtime
|
|
3
|
+
* the interactive REPL uses. Prints the response (or a structured result with --json).
|
|
4
|
+
*/
|
|
5
|
+
export interface RunOptions {
|
|
6
|
+
mode?: string;
|
|
7
|
+
config?: string;
|
|
8
|
+
json?: boolean;
|
|
9
|
+
dryRun?: boolean;
|
|
10
|
+
}
|
|
11
|
+
export declare function runCommand(input: string, options: RunOptions): Promise<void>;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `ai-runtime run "<input>"` — one-shot Runtime invocation for CI/scripts. Shares the exact Runtime
|
|
3
|
+
* the interactive REPL uses. Prints the response (or a structured result with --json).
|
|
4
|
+
*/
|
|
5
|
+
import { Runtime } from '../../runtime/runtime.js';
|
|
6
|
+
import { print } from '../render.js';
|
|
7
|
+
import { RUNTIME_MODES } from '../../runtime/types.js';
|
|
8
|
+
export async function runCommand(input, options) {
|
|
9
|
+
const rt = await Runtime.load({ ...(options.config ? { config: options.config } : {}) });
|
|
10
|
+
const req = { input };
|
|
11
|
+
if (options.dryRun)
|
|
12
|
+
req.dryRun = true;
|
|
13
|
+
if (options.mode) {
|
|
14
|
+
if (!RUNTIME_MODES.includes(options.mode)) {
|
|
15
|
+
print(`invalid mode '${options.mode}'. valid: ${RUNTIME_MODES.join(', ')}`);
|
|
16
|
+
process.exitCode = 1;
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
req.mode = options.mode;
|
|
20
|
+
}
|
|
21
|
+
const result = await rt.run(req);
|
|
22
|
+
if (options.json)
|
|
23
|
+
return print(JSON.stringify(result, null, 2));
|
|
24
|
+
if (result.response?.text)
|
|
25
|
+
print(result.response.text);
|
|
26
|
+
else if (result.response?.json !== undefined)
|
|
27
|
+
print(JSON.stringify(result.response.json, null, 2));
|
|
28
|
+
if (result.mode.executed !== result.mode.selected)
|
|
29
|
+
print(`(mode: ${result.mode.selected} → ${result.mode.executed})`);
|
|
30
|
+
if (result.clarification)
|
|
31
|
+
print(`? ${result.clarification.question}`);
|
|
32
|
+
if (result.error)
|
|
33
|
+
print(`[${result.error.category}] ${result.error.message}`);
|
|
34
|
+
// Any un-ok result (a router error OR a failed plan/execute/orchestrate) is a non-zero exit for CI.
|
|
35
|
+
if (!result.ok)
|
|
36
|
+
process.exitCode = 1;
|
|
37
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `ai-runtime setup` — guided bootstrap. Detects the workspace and which provider keys are already in the
|
|
3
|
+
* environment, then generates `.ai-runtime/config.yaml`, a commented `.env.example`, and .gitignore
|
|
4
|
+
* entries — ASKING before every disk mutation. It never installs anything, never creates accounts, and
|
|
5
|
+
* never writes a secret VALUE (config names env vars only). With no keys present it prints onboarding
|
|
6
|
+
* recommendations instead. The detection half is pure and unit-tested; the command half drives it with
|
|
7
|
+
* confirmations.
|
|
8
|
+
*/
|
|
9
|
+
export interface Candidate {
|
|
10
|
+
id: string;
|
|
11
|
+
kind: string;
|
|
12
|
+
apiKeyEnv?: string;
|
|
13
|
+
local?: boolean;
|
|
14
|
+
link: string;
|
|
15
|
+
}
|
|
16
|
+
/** The providers setup recommends. Cloud ones need a key (env-var name only); Ollama is local/no-key. */
|
|
17
|
+
export declare const SETUP_CANDIDATES: Candidate[];
|
|
18
|
+
export interface SetupDetection {
|
|
19
|
+
workspaceSummary: string;
|
|
20
|
+
configFile?: string;
|
|
21
|
+
/** Providers usable now: a cloud provider whose key is set, plus local ones. */
|
|
22
|
+
ready: Candidate[];
|
|
23
|
+
/** Cloud providers whose key is absent (shown as onboarding recommendations). */
|
|
24
|
+
missing: Candidate[];
|
|
25
|
+
}
|
|
26
|
+
/** Detect what setup would configure. Pure: reads the filesystem + env, writes nothing. */
|
|
27
|
+
export declare function detectSetup(cwd: string, env: NodeJS.ProcessEnv): SetupDetection;
|
|
28
|
+
/** Generate `.ai-runtime/config.yaml` naming only the ready providers (env-var names, never values). */
|
|
29
|
+
export declare function generateConfigYaml(ready: Candidate[]): string;
|
|
30
|
+
/** Generate a commented `.env.example` for the recommended cloud providers. */
|
|
31
|
+
export declare function generateEnvExample(): string;
|
|
32
|
+
export declare function setupCommand(opts?: {
|
|
33
|
+
yes?: boolean;
|
|
34
|
+
}): Promise<void>;
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `ai-runtime setup` — guided bootstrap. Detects the workspace and which provider keys are already in the
|
|
3
|
+
* environment, then generates `.ai-runtime/config.yaml`, a commented `.env.example`, and .gitignore
|
|
4
|
+
* entries — ASKING before every disk mutation. It never installs anything, never creates accounts, and
|
|
5
|
+
* never writes a secret VALUE (config names env vars only). With no keys present it prints onboarding
|
|
6
|
+
* recommendations instead. The detection half is pure and unit-tested; the command half drives it with
|
|
7
|
+
* confirmations.
|
|
8
|
+
*/
|
|
9
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
10
|
+
import { join, resolve } from 'node:path';
|
|
11
|
+
import { detectWorkspace, summarizeWorkspace } from '../../runtime/workspace/workspace.js';
|
|
12
|
+
import { findRuntimeConfigFile } from '../../runtime/config.js';
|
|
13
|
+
import { findConfigFile } from '../../config/load.js';
|
|
14
|
+
import { confirm } from '../prompt.js';
|
|
15
|
+
import { print } from '../render.js';
|
|
16
|
+
/** The providers setup recommends. Cloud ones need a key (env-var name only); Ollama is local/no-key. */
|
|
17
|
+
export const SETUP_CANDIDATES = [
|
|
18
|
+
{ id: 'gemini', kind: 'gemini', apiKeyEnv: 'GEMINI_API_KEY', link: 'https://aistudio.google.com/apikey' },
|
|
19
|
+
{ id: 'groq', kind: 'groq', apiKeyEnv: 'GROQ_API_KEY', link: 'https://console.groq.com/keys' },
|
|
20
|
+
{ id: 'anthropic', kind: 'anthropic', apiKeyEnv: 'ANTHROPIC_API_KEY', link: 'https://console.anthropic.com/settings/keys' },
|
|
21
|
+
{ id: 'openrouter', kind: 'openrouter', apiKeyEnv: 'OPENROUTER_API_KEY', link: 'https://openrouter.ai/keys' },
|
|
22
|
+
{ id: 'ollama', kind: 'ollama', local: true, link: 'https://ollama.com' },
|
|
23
|
+
];
|
|
24
|
+
/** Detect what setup would configure. Pure: reads the filesystem + env, writes nothing. */
|
|
25
|
+
export function detectSetup(cwd, env) {
|
|
26
|
+
const ws = detectWorkspace(cwd);
|
|
27
|
+
const configFile = findRuntimeConfigFile(cwd) ?? findConfigFile(cwd);
|
|
28
|
+
const ready = SETUP_CANDIDATES.filter((c) => c.local || (c.apiKeyEnv && (env[c.apiKeyEnv] ?? '') !== ''));
|
|
29
|
+
const missing = SETUP_CANDIDATES.filter((c) => c.apiKeyEnv && (env[c.apiKeyEnv] ?? '') === '');
|
|
30
|
+
return { workspaceSummary: ws ? summarizeWorkspace(ws) : '(no workspace traits detected)', ...(configFile ? { configFile } : {}), ready, missing };
|
|
31
|
+
}
|
|
32
|
+
/** Generate `.ai-runtime/config.yaml` naming only the ready providers (env-var names, never values). */
|
|
33
|
+
export function generateConfigYaml(ready) {
|
|
34
|
+
const blocks = ready.map((c) => (c.local ? ` - id: ${c.id}\n kind: ${c.kind}` : ` - id: ${c.id}\n kind: ${c.kind}\n apiKeyEnv: ${c.apiKeyEnv}`));
|
|
35
|
+
const body = blocks.length ? blocks.join('\n\n') : ' # No providers detected yet. Add one, e.g.:\n # - id: gemini\n # kind: gemini\n # apiKeyEnv: GEMINI_API_KEY';
|
|
36
|
+
return `# ai-runtime configuration. Secret VALUES live in .env (gitignored); this file names env vars only.\nruntime:\n defaultMode: auto\n\nproviders:\n${body}\n`;
|
|
37
|
+
}
|
|
38
|
+
/** Generate a commented `.env.example` for the recommended cloud providers. */
|
|
39
|
+
export function generateEnvExample() {
|
|
40
|
+
const lines = ['# Fill in the keys you have; leave the rest blank. The runtime adapts to whatever is configured.'];
|
|
41
|
+
for (const c of SETUP_CANDIDATES)
|
|
42
|
+
if (c.apiKeyEnv)
|
|
43
|
+
lines.push(`${c.apiKeyEnv}=`);
|
|
44
|
+
return lines.join('\n') + '\n';
|
|
45
|
+
}
|
|
46
|
+
const GITIGNORE_ENTRIES = ['.env', '.env.*', '!.env.example', '.ai-runtime/'];
|
|
47
|
+
function missingGitignore(cwd) {
|
|
48
|
+
const full = resolve(cwd, '.gitignore');
|
|
49
|
+
const existing = existsSync(full) ? readFileSync(full, 'utf8').split('\n') : [];
|
|
50
|
+
return GITIGNORE_ENTRIES.filter((e) => !existing.includes(e));
|
|
51
|
+
}
|
|
52
|
+
function renderOnboarding(missing) {
|
|
53
|
+
const lines = ['', 'To add a provider, get a key and set its env var (or install Ollama for a local, no-key option):'];
|
|
54
|
+
for (const c of missing)
|
|
55
|
+
lines.push(` • ${c.id.padEnd(11)} ${c.apiKeyEnv ? `${c.apiKeyEnv} → ` : ''}${c.link}`);
|
|
56
|
+
lines.push(` • ollama local, no key → https://ollama.com`);
|
|
57
|
+
return lines;
|
|
58
|
+
}
|
|
59
|
+
export async function setupCommand(opts = {}) {
|
|
60
|
+
const cwd = process.cwd();
|
|
61
|
+
const env = process.env;
|
|
62
|
+
const det = detectSetup(cwd, env);
|
|
63
|
+
print('ai-runtime setup\n');
|
|
64
|
+
print(`workspace: ${det.workspaceSummary}`);
|
|
65
|
+
print(det.ready.length ? `ready: ${det.ready.map((c) => c.id).join(', ')}` : 'ready: (no providers configured yet)');
|
|
66
|
+
const ask = async (q) => opts.yes || (await confirm(q));
|
|
67
|
+
// 1) config file
|
|
68
|
+
const configTarget = join(cwd, '.ai-runtime', 'config.yaml');
|
|
69
|
+
if (existsSync(configTarget))
|
|
70
|
+
print(`\n• ${configTarget} exists — left unchanged`);
|
|
71
|
+
else if (await ask(`\nWrite ${configTarget}?`)) {
|
|
72
|
+
mkdirSync(join(cwd, '.ai-runtime'), { recursive: true });
|
|
73
|
+
writeFileSync(configTarget, generateConfigYaml(det.ready));
|
|
74
|
+
print(` ✓ wrote .ai-runtime/config.yaml`);
|
|
75
|
+
}
|
|
76
|
+
else
|
|
77
|
+
print(' • skipped config');
|
|
78
|
+
// 2) .env.example
|
|
79
|
+
const envTarget = join(cwd, '.env.example');
|
|
80
|
+
if (existsSync(envTarget))
|
|
81
|
+
print(`• ${envTarget} exists — left unchanged`);
|
|
82
|
+
else if (await ask(`Write ${envTarget}?`)) {
|
|
83
|
+
writeFileSync(envTarget, generateEnvExample());
|
|
84
|
+
print(' ✓ wrote .env.example');
|
|
85
|
+
}
|
|
86
|
+
else
|
|
87
|
+
print(' • skipped .env.example');
|
|
88
|
+
// 3) .gitignore
|
|
89
|
+
const missing = missingGitignore(cwd);
|
|
90
|
+
if (!missing.length)
|
|
91
|
+
print('• .gitignore already covers secrets + local state');
|
|
92
|
+
else if (await ask(`Add ${missing.length} entr${missing.length === 1 ? 'y' : 'ies'} to .gitignore?`)) {
|
|
93
|
+
const full = resolve(cwd, '.gitignore');
|
|
94
|
+
const existing = existsSync(full) ? readFileSync(full, 'utf8') : '';
|
|
95
|
+
writeFileSync(full, existing + `${existing && !existing.endsWith('\n') ? '\n' : ''}\n# ai-runtime\n${missing.join('\n')}\n`);
|
|
96
|
+
print(` ✓ updated .gitignore`);
|
|
97
|
+
}
|
|
98
|
+
else
|
|
99
|
+
print(' • skipped .gitignore');
|
|
100
|
+
if (det.missing.length)
|
|
101
|
+
for (const line of renderOnboarding(det.missing))
|
|
102
|
+
print(line);
|
|
103
|
+
print('\nNext: copy .env.example to .env, fill in a key, then run `ai-runtime doctor`.');
|
|
104
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `ai-runtime skills` — list the skills the runtime has (built-in + auto-loaded from `.ai-runtime/skills/`
|
|
3
|
+
* and configured paths). `--discover` scans the rest of the workspace for skill-looking files and REPORTS
|
|
4
|
+
* them without loading — the user enables one by moving it into `.ai-runtime/skills/` or adding its path to
|
|
5
|
+
* `skills.paths` in config (loading a module runs its code, so it is never implicit).
|
|
6
|
+
*/
|
|
7
|
+
import { Runtime } from '../../runtime/runtime.js';
|
|
8
|
+
export interface SkillsReport {
|
|
9
|
+
loaded: Array<{
|
|
10
|
+
id: string;
|
|
11
|
+
version: string;
|
|
12
|
+
source?: string;
|
|
13
|
+
}>;
|
|
14
|
+
sourceErrors: Array<{
|
|
15
|
+
path: string;
|
|
16
|
+
detail: string;
|
|
17
|
+
}>;
|
|
18
|
+
candidates: Array<{
|
|
19
|
+
relative: string;
|
|
20
|
+
kind: string;
|
|
21
|
+
}>;
|
|
22
|
+
}
|
|
23
|
+
export declare function gatherSkills(rt: Runtime, discover: boolean): Promise<SkillsReport>;
|
|
24
|
+
export declare function skillsCommand(opts: {
|
|
25
|
+
config?: string;
|
|
26
|
+
discover?: boolean;
|
|
27
|
+
json?: boolean;
|
|
28
|
+
}): Promise<void>;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `ai-runtime skills` — list the skills the runtime has (built-in + auto-loaded from `.ai-runtime/skills/`
|
|
3
|
+
* and configured paths). `--discover` scans the rest of the workspace for skill-looking files and REPORTS
|
|
4
|
+
* them without loading — the user enables one by moving it into `.ai-runtime/skills/` or adding its path to
|
|
5
|
+
* `skills.paths` in config (loading a module runs its code, so it is never implicit).
|
|
6
|
+
*/
|
|
7
|
+
import { Runtime } from '../../runtime/runtime.js';
|
|
8
|
+
import { print } from '../render.js';
|
|
9
|
+
export async function gatherSkills(rt, discover) {
|
|
10
|
+
const sources = rt.skillSources();
|
|
11
|
+
const sourceById = new Map();
|
|
12
|
+
for (const s of sources)
|
|
13
|
+
for (const sk of s.skills)
|
|
14
|
+
sourceById.set(sk.id, s.source.relative);
|
|
15
|
+
return {
|
|
16
|
+
loaded: rt.skills().map((s) => ({ id: s.id, version: s.version, ...(sourceById.get(s.id) ? { source: sourceById.get(s.id) } : {}) })),
|
|
17
|
+
sourceErrors: sources.filter((s) => s.status === 'error').map((s) => ({ path: s.source.relative, detail: s.detail ?? 'unknown error' })),
|
|
18
|
+
candidates: discover ? rt.discoverSkillCandidates().map((c) => ({ relative: c.relative, kind: c.kind })) : [],
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
function renderSkills(r, discover) {
|
|
22
|
+
const lines = ['Skills:'];
|
|
23
|
+
if (!r.loaded.length)
|
|
24
|
+
lines.push(' (none available — a skill needs its tools registered)');
|
|
25
|
+
for (const s of r.loaded)
|
|
26
|
+
lines.push(` ${s.id.padEnd(20)} v${s.version}${s.source ? ` (from ${s.source})` : ' (built-in)'}`);
|
|
27
|
+
if (r.sourceErrors.length) {
|
|
28
|
+
lines.push('', 'Skipped (could not load):');
|
|
29
|
+
for (const e of r.sourceErrors)
|
|
30
|
+
lines.push(` ✗ ${e.path} — ${e.detail}`);
|
|
31
|
+
}
|
|
32
|
+
if (discover) {
|
|
33
|
+
lines.push('', r.candidates.length ? `Discovered ${r.candidates.length} candidate(s) elsewhere in the workspace (not loaded):` : 'No other skill files found in the workspace.');
|
|
34
|
+
for (const c of r.candidates)
|
|
35
|
+
lines.push(` • ${c.relative} [${c.kind}]`);
|
|
36
|
+
if (r.candidates.length)
|
|
37
|
+
lines.push('', 'To enable one: move it into .ai-runtime/skills/, or add its path to `skills.paths` in .ai-runtime/config.yaml.');
|
|
38
|
+
}
|
|
39
|
+
return lines;
|
|
40
|
+
}
|
|
41
|
+
export async function skillsCommand(opts) {
|
|
42
|
+
const rt = await Runtime.load({ ...(opts.config ? { config: opts.config } : {}) });
|
|
43
|
+
const report = await gatherSkills(rt, !!opts.discover);
|
|
44
|
+
if (opts.json)
|
|
45
|
+
return print(JSON.stringify(report, null, 2));
|
|
46
|
+
for (const line of renderSkills(report, !!opts.discover))
|
|
47
|
+
print(line);
|
|
48
|
+
}
|