agent-devkit 0.2.0 → 0.3.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/README.md +48 -6
- package/bin/agent.mjs +133 -7
- package/package.json +1 -1
- package/runtime/README.md +187 -5
- package/runtime/agent +31 -5
- package/runtime/agents/README.md +18 -0
- package/runtime/agents/contribution-reviewer/AGENTS.md +8 -0
- package/runtime/agents/contribution-reviewer/README.md +8 -0
- package/runtime/agents/contribution-reviewer/agent.yaml +40 -0
- package/runtime/agents/contribution-reviewer/capabilities/plan-contribution-pr/capability.yaml +27 -0
- package/runtime/agents/contribution-reviewer/capabilities/plan-contribution-pr/decision-rules.md +5 -0
- package/runtime/agents/contribution-reviewer/capabilities/plan-contribution-pr/workflow.md +6 -0
- package/runtime/agents/contribution-reviewer/capabilities/review-contribution/capability.yaml +25 -0
- package/runtime/agents/contribution-reviewer/capabilities/review-contribution/decision-rules.md +5 -0
- package/runtime/agents/contribution-reviewer/capabilities/review-contribution/workflow.md +5 -0
- package/runtime/agents/contribution-reviewer/capabilities/validate-local-contribution/capability.yaml +26 -0
- package/runtime/agents/contribution-reviewer/capabilities/validate-local-contribution/decision-rules.md +5 -0
- package/runtime/agents/contribution-reviewer/capabilities/validate-local-contribution/workflow.md +6 -0
- package/runtime/agents/contribution-reviewer/infra/README.md +6 -0
- package/runtime/agents/contribution-reviewer/knowledge/context.md +8 -0
- package/runtime/agents/contribution-reviewer/knowledge/system.md +8 -0
- package/runtime/agents/contribution-reviewer/templates/README.md +3 -0
- package/runtime/agents/knowledge-author/AGENTS.md +7 -0
- package/runtime/agents/knowledge-author/README.md +7 -0
- package/runtime/agents/knowledge-author/agent.yaml +37 -0
- package/runtime/agents/knowledge-author/capabilities/create-knowledge-snapshot/capability.yaml +30 -0
- package/runtime/agents/knowledge-author/capabilities/create-knowledge-snapshot/decision-rules.md +6 -0
- package/runtime/agents/knowledge-author/capabilities/create-knowledge-snapshot/workflow.md +7 -0
- package/runtime/agents/knowledge-author/infra/.gitkeep +1 -0
- package/runtime/agents/knowledge-author/knowledge/context.md +4 -0
- package/runtime/agents/knowledge-author/knowledge/system.md +4 -0
- package/runtime/agents/knowledge-author/templates/.gitkeep +1 -0
- package/runtime/agents/knowledge-curator/AGENTS.md +7 -0
- package/runtime/agents/knowledge-curator/README.md +6 -0
- package/runtime/agents/knowledge-curator/agent.yaml +37 -0
- package/runtime/agents/knowledge-curator/capabilities/curate-knowledge-base/capability.yaml +29 -0
- package/runtime/agents/knowledge-curator/capabilities/curate-knowledge-base/decision-rules.md +6 -0
- package/runtime/agents/knowledge-curator/capabilities/curate-knowledge-base/workflow.md +7 -0
- package/runtime/agents/knowledge-curator/infra/.gitkeep +1 -0
- package/runtime/agents/knowledge-curator/knowledge/context.md +4 -0
- package/runtime/agents/knowledge-curator/knowledge/system.md +4 -0
- package/runtime/agents/knowledge-curator/templates/.gitkeep +1 -0
- package/runtime/agents/knowledge-infra-builder/AGENTS.md +8 -0
- package/runtime/agents/knowledge-infra-builder/README.md +8 -0
- package/runtime/agents/knowledge-infra-builder/agent.yaml +38 -0
- package/runtime/agents/knowledge-infra-builder/capabilities/create-knowledge-base/capability.yaml +30 -0
- package/runtime/agents/knowledge-infra-builder/capabilities/create-knowledge-base/decision-rules.md +6 -0
- package/runtime/agents/knowledge-infra-builder/capabilities/create-knowledge-base/workflow.md +7 -0
- package/runtime/agents/knowledge-infra-builder/infra/.gitkeep +1 -0
- package/runtime/agents/knowledge-infra-builder/knowledge/context.md +4 -0
- package/runtime/agents/knowledge-infra-builder/knowledge/system.md +4 -0
- package/runtime/agents/knowledge-infra-builder/templates/.gitkeep +1 -0
- package/runtime/agents/knowledge-owner/AGENTS.md +7 -0
- package/runtime/agents/knowledge-owner/README.md +6 -0
- package/runtime/agents/knowledge-owner/agent.yaml +37 -0
- package/runtime/agents/knowledge-owner/capabilities/publish-knowledge-snapshot/capability.yaml +28 -0
- package/runtime/agents/knowledge-owner/capabilities/publish-knowledge-snapshot/decision-rules.md +6 -0
- package/runtime/agents/knowledge-owner/capabilities/publish-knowledge-snapshot/workflow.md +7 -0
- package/runtime/agents/knowledge-owner/infra/.gitkeep +1 -0
- package/runtime/agents/knowledge-owner/knowledge/context.md +4 -0
- package/runtime/agents/knowledge-owner/knowledge/system.md +4 -0
- package/runtime/agents/knowledge-owner/templates/.gitkeep +1 -0
- package/runtime/agents/knowledge-reviewer/AGENTS.md +7 -0
- package/runtime/agents/knowledge-reviewer/README.md +7 -0
- package/runtime/agents/knowledge-reviewer/agent.yaml +36 -0
- package/runtime/agents/knowledge-reviewer/capabilities/review-knowledge-snapshot/capability.yaml +26 -0
- package/runtime/agents/knowledge-reviewer/capabilities/review-knowledge-snapshot/decision-rules.md +6 -0
- package/runtime/agents/knowledge-reviewer/capabilities/review-knowledge-snapshot/workflow.md +7 -0
- package/runtime/agents/knowledge-reviewer/infra/.gitkeep +1 -0
- package/runtime/agents/knowledge-reviewer/knowledge/context.md +4 -0
- package/runtime/agents/knowledge-reviewer/knowledge/system.md +4 -0
- package/runtime/agents/knowledge-reviewer/templates/.gitkeep +1 -0
- package/runtime/agents/local-memory-manager/AGENTS.md +5 -0
- package/runtime/agents/local-memory-manager/README.md +7 -0
- package/runtime/agents/local-memory-manager/agent.yaml +38 -0
- package/runtime/agents/local-memory-manager/capabilities/curate-local-memory/capability.yaml +19 -0
- package/runtime/agents/local-memory-manager/capabilities/curate-local-memory/decision-rules.md +5 -0
- package/runtime/agents/local-memory-manager/capabilities/curate-local-memory/workflow.md +6 -0
- package/runtime/agents/local-memory-manager/capabilities/inspect-local-memory/capability.yaml +19 -0
- package/runtime/agents/local-memory-manager/capabilities/inspect-local-memory/decision-rules.md +5 -0
- package/runtime/agents/local-memory-manager/capabilities/inspect-local-memory/workflow.md +5 -0
- package/runtime/agents/local-memory-manager/infra/.gitkeep +1 -0
- package/runtime/agents/local-memory-manager/knowledge/context.md +4 -0
- package/runtime/agents/local-memory-manager/knowledge/system.md +4 -0
- package/runtime/agents/local-memory-manager/templates/.gitkeep +1 -0
- package/runtime/agents/memory-sync-manager/AGENTS.md +7 -0
- package/runtime/agents/memory-sync-manager/README.md +7 -0
- package/runtime/agents/memory-sync-manager/agent.yaml +37 -0
- package/runtime/agents/memory-sync-manager/capabilities/plan-memory-backup/capability.yaml +29 -0
- package/runtime/agents/memory-sync-manager/capabilities/plan-memory-backup/decision-rules.md +6 -0
- package/runtime/agents/memory-sync-manager/capabilities/plan-memory-backup/workflow.md +7 -0
- package/runtime/agents/memory-sync-manager/infra/.gitkeep +1 -0
- package/runtime/agents/memory-sync-manager/knowledge/context.md +4 -0
- package/runtime/agents/memory-sync-manager/knowledge/system.md +4 -0
- package/runtime/agents/memory-sync-manager/templates/.gitkeep +1 -0
- package/runtime/agents/shared-memory-curator/AGENTS.md +5 -0
- package/runtime/agents/shared-memory-curator/README.md +6 -0
- package/runtime/agents/shared-memory-curator/agent.yaml +38 -0
- package/runtime/agents/shared-memory-curator/capabilities/create-shared-memory/capability.yaml +19 -0
- package/runtime/agents/shared-memory-curator/capabilities/create-shared-memory/decision-rules.md +5 -0
- package/runtime/agents/shared-memory-curator/capabilities/create-shared-memory/workflow.md +5 -0
- package/runtime/agents/shared-memory-curator/capabilities/publish-shared-submission/capability.yaml +19 -0
- package/runtime/agents/shared-memory-curator/capabilities/publish-shared-submission/decision-rules.md +5 -0
- package/runtime/agents/shared-memory-curator/capabilities/publish-shared-submission/workflow.md +5 -0
- package/runtime/agents/shared-memory-curator/capabilities/review-shared-submission/capability.yaml +19 -0
- package/runtime/agents/shared-memory-curator/capabilities/review-shared-submission/decision-rules.md +5 -0
- package/runtime/agents/shared-memory-curator/capabilities/review-shared-submission/workflow.md +5 -0
- package/runtime/agents/shared-memory-curator/infra/.gitkeep +1 -0
- package/runtime/agents/shared-memory-curator/knowledge/context.md +5 -0
- package/runtime/agents/shared-memory-curator/knowledge/system.md +4 -0
- package/runtime/agents/shared-memory-curator/templates/.gitkeep +1 -0
- package/runtime/cli/README.md +35 -4
- package/runtime/cli/aikit/__init__.py +1 -1
- package/runtime/cli/aikit/agent_registry.py +4 -2
- package/runtime/cli/aikit/agentic_commands.py +158 -0
- package/runtime/cli/aikit/app_home.py +1 -0
- package/runtime/cli/aikit/audit.py +16 -6
- package/runtime/cli/aikit/catalog.py +278 -8
- package/runtime/cli/aikit/cli_dispatch.py +489 -13
- package/runtime/cli/aikit/cli_parser.py +145 -7
- package/runtime/cli/aikit/contribution.py +132 -2
- package/runtime/cli/aikit/doctor_runtime.py +85 -0
- package/runtime/cli/aikit/eval.py +356 -10
- package/runtime/cli/aikit/human_output.py +310 -4
- package/runtime/cli/aikit/interactive_wizard.py +148 -0
- package/runtime/cli/aikit/knowledge_base.py +1067 -0
- package/runtime/cli/aikit/llm.py +12 -4
- package/runtime/cli/aikit/local_artifacts.py +444 -0
- package/runtime/cli/aikit/local_llm.py +161 -0
- package/runtime/cli/aikit/main.py +15 -0
- package/runtime/cli/aikit/mcp_manifest.py +798 -0
- package/runtime/cli/aikit/mcp_tools.py +643 -5
- package/runtime/cli/aikit/memory.py +405 -0
- package/runtime/cli/aikit/mini_brain.py +20 -1
- package/runtime/cli/aikit/natural_prompt_runtime.py +125 -1
- package/runtime/cli/aikit/ollama.py +64 -15
- package/runtime/cli/aikit/onboarding.py +551 -0
- package/runtime/cli/aikit/output.py +67 -0
- package/runtime/cli/aikit/prompt_injection.py +12 -1
- package/runtime/cli/aikit/roadmap_cli.py +1 -1
- package/runtime/cli/aikit/secrets.py +3 -2
- package/runtime/cli/aikit/setup_wizard_payload.py +3 -0
- package/runtime/cli/aikit/shared_memory.py +415 -0
- package/runtime/cli/aikit/specialist_readiness.py +152 -0
- package/runtime/cli/aikit/tasks.py +104 -1
- package/runtime/cli/aikit/team.py +380 -0
- package/runtime/cli/aikit/toolchain.py +7 -2
- package/runtime/cli/aikit/workflows.py +115 -14
- package/runtime/providers/knowledge-github.yaml +40 -0
- package/runtime/providers/knowledge-google-drive.yaml +32 -0
- package/runtime/providers/knowledge-local.yaml +26 -0
- package/runtime/providers/knowledge-notion.yaml +32 -0
- package/runtime/providers/knowledge-obsidian.yaml +24 -0
- package/runtime/providers/knowledge-onedrive.yaml +36 -0
- package/runtime/providers/knowledge-s3.yaml +45 -0
- package/runtime/providers/knowledge-sharepoint.yaml +39 -0
- package/runtime/providers/knowledge-supabase.yaml +43 -0
- package/runtime/providers/knowledge-vector.yaml +39 -0
- package/runtime/requirements.txt +6 -0
- package/runtime/scripts/docker-cli-qa.sh +453 -0
- package/runtime/scripts/release-catalog-snapshot.json +55 -4
- package/runtime/scripts/release-gate.py +54 -13
- package/runtime/tooling/toolchain.yaml +92 -0
- package/runtime/vendor/skills/napkin/napkin.md +21 -7
- package/runtime/workflows/azure-card-analysis/README.md +3 -0
- package/runtime/workflows/azure-card-analysis/workflow.yaml +30 -0
- package/runtime/workflows/daily-pr-review/README.md +3 -0
- package/runtime/workflows/daily-pr-review/workflow.yaml +31 -0
- package/runtime/workflows/incident-analysis/README.md +3 -0
- package/runtime/workflows/incident-analysis/workflow.yaml +33 -0
- package/runtime/workflows/release-prep/README.md +3 -0
- package/runtime/workflows/release-prep/workflow.yaml +30 -0
package/runtime/cli/aikit/llm.py
CHANGED
|
@@ -77,7 +77,7 @@ BACKENDS: dict[str, LlmBackend] = {
|
|
|
77
77
|
base_url_env="OLLAMA_BASE_URL",
|
|
78
78
|
model_env="OLLAMA_MODEL",
|
|
79
79
|
default_base_url="http://localhost:11434/v1",
|
|
80
|
-
default_model="
|
|
80
|
+
default_model="qwen3:0.6b",
|
|
81
81
|
notes="Uses a local Ollama server through an OpenAI-compatible endpoint.",
|
|
82
82
|
),
|
|
83
83
|
"codex-cli": LlmBackend(
|
|
@@ -387,18 +387,26 @@ def doctor_backend(backend: LlmBackend, config: dict[str, Any]) -> dict[str, Any
|
|
|
387
387
|
}
|
|
388
388
|
|
|
389
389
|
if backend.auth == "none":
|
|
390
|
-
|
|
390
|
+
env_base_url = env_value(backend.base_url_env)
|
|
391
|
+
base_url = configured.get("base_url") or env_base_url or backend.default_base_url
|
|
391
392
|
model = configured.get("model") or env_value(backend.model_env) or backend.default_model
|
|
393
|
+
binary = shutil.which(backend.id) if backend.id == "ollama" else None
|
|
394
|
+
local_available = bool(configured or env_base_url or binary)
|
|
392
395
|
return {
|
|
393
396
|
"id": backend.id,
|
|
394
397
|
"display_name": backend.display_name,
|
|
395
398
|
"kind": backend.kind,
|
|
396
|
-
"status": "ok" if
|
|
399
|
+
"status": "ok" if local_available else "missing",
|
|
397
400
|
"configured": bool(configured),
|
|
398
401
|
"base_url": base_url,
|
|
399
402
|
"model": model,
|
|
403
|
+
"binary": binary,
|
|
400
404
|
"health": "unchecked",
|
|
401
|
-
"message":
|
|
405
|
+
"message": (
|
|
406
|
+
"Local backend configured; daemon health is not probed by default."
|
|
407
|
+
if local_available
|
|
408
|
+
else "Local backend is not configured and no local binary was found in PATH."
|
|
409
|
+
),
|
|
402
410
|
}
|
|
403
411
|
|
|
404
412
|
api_key_env = configured.get("api_key_env") or backend.api_key_env
|
|
@@ -0,0 +1,444 @@
|
|
|
1
|
+
"""Local user-created skills, scripts and agent scaffolds."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import re
|
|
7
|
+
import shutil
|
|
8
|
+
import subprocess
|
|
9
|
+
from datetime import datetime, timezone
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from cli.aikit.app_home import app_home, ensure_app_home
|
|
14
|
+
from cli.aikit.errors import DevKitError
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
LOCAL_ARTIFACT_SCHEMA_VERSION = "agent-devkit.local-artifacts/v1"
|
|
18
|
+
ID_PATTERN = re.compile(r"^[a-z0-9][a-z0-9._-]*$")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def skill_create(skill_id: str | None, *, description: str | None = None, force: bool = False) -> dict[str, Any]:
|
|
22
|
+
item_id = require_id(skill_id, "skill id")
|
|
23
|
+
path = local_home("skills") / item_id
|
|
24
|
+
if path.exists() and not force:
|
|
25
|
+
raise DevKitError(f"local skill already exists: {item_id}")
|
|
26
|
+
path.mkdir(parents=True, exist_ok=True)
|
|
27
|
+
skill_path = path / "SKILL.md"
|
|
28
|
+
if force or not skill_path.exists():
|
|
29
|
+
skill_path.write_text(render_skill(item_id, description), encoding="utf-8")
|
|
30
|
+
return local_artifact_payload("local-skill", "created", item_id, path, write_policy="local_config_write")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def skill_list() -> dict[str, Any]:
|
|
34
|
+
return local_artifact_list("local-skills", "skills", marker="SKILL.md")
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def skill_show(skill_id: str | None) -> dict[str, Any]:
|
|
38
|
+
return local_artifact_show("local-skill", "skills", require_id(skill_id, "skill id"), marker="SKILL.md")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def skill_update(skill_id: str | None, *, description: str | None = None) -> dict[str, Any]:
|
|
42
|
+
item_id = require_id(skill_id, "skill id")
|
|
43
|
+
path = local_home("skills") / item_id
|
|
44
|
+
if not path.exists():
|
|
45
|
+
raise DevKitError(f"local skill not found: {item_id}")
|
|
46
|
+
(path / "SKILL.md").write_text(render_skill(item_id, description), encoding="utf-8")
|
|
47
|
+
return local_artifact_payload("local-skill", "updated", item_id, path, write_policy="local_config_write")
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def skill_delete(skill_id: str | None, *, yes: bool = False) -> dict[str, Any]:
|
|
51
|
+
item_id = require_id(skill_id, "skill id")
|
|
52
|
+
path = local_home("skills") / item_id
|
|
53
|
+
if not yes:
|
|
54
|
+
payload = local_artifact_payload("local-skill", "needs-confirmation", item_id, path, write_policy="local_config_write")
|
|
55
|
+
payload["ok"] = False
|
|
56
|
+
payload["exit_code"] = 2
|
|
57
|
+
return payload
|
|
58
|
+
shutil.rmtree(path, ignore_errors=True)
|
|
59
|
+
return local_artifact_payload("local-skill", "deleted", item_id, path, write_policy="local_config_write")
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def script_create(script_id: str | None, *, command: str | None = None, force: bool = False) -> dict[str, Any]:
|
|
63
|
+
item_id = require_id(script_id, "script id")
|
|
64
|
+
path = local_home("scripts") / f"{item_id}.sh"
|
|
65
|
+
if path.exists() and not force:
|
|
66
|
+
raise DevKitError(f"local script already exists: {item_id}")
|
|
67
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
68
|
+
path.write_text(render_script(command), encoding="utf-8")
|
|
69
|
+
path.chmod(0o755)
|
|
70
|
+
return local_artifact_payload("local-script", "created", item_id, path, write_policy="local_write")
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def script_list() -> dict[str, Any]:
|
|
74
|
+
home = local_home("scripts")
|
|
75
|
+
items = [
|
|
76
|
+
local_artifact_item(path.stem, path, "local-script", enabled=True)
|
|
77
|
+
for path in sorted(home.glob("*.sh"))
|
|
78
|
+
]
|
|
79
|
+
return {"kind": "local-scripts", "schema_version": LOCAL_ARTIFACT_SCHEMA_VERSION, "status": "ok", "home": str(home), "items": items}
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def script_run(script_id: str | None, *, dry_run: bool = False, yes: bool = False) -> dict[str, Any]:
|
|
83
|
+
item_id = require_id(script_id, "script id")
|
|
84
|
+
path = local_home("scripts") / f"{item_id}.sh"
|
|
85
|
+
if not path.exists():
|
|
86
|
+
raise DevKitError(f"local script not found: {item_id}")
|
|
87
|
+
if dry_run or not yes:
|
|
88
|
+
return {
|
|
89
|
+
"kind": "local-script-run",
|
|
90
|
+
"schema_version": LOCAL_ARTIFACT_SCHEMA_VERSION,
|
|
91
|
+
"status": "planned" if dry_run else "needs-confirmation",
|
|
92
|
+
"ok": bool(dry_run),
|
|
93
|
+
"id": item_id,
|
|
94
|
+
"path": str(path),
|
|
95
|
+
"command": [str(path)],
|
|
96
|
+
"write_policy": "local_write",
|
|
97
|
+
"message": "Use --yes to run the local script." if not dry_run and not yes else "Dry-run only.",
|
|
98
|
+
**({} if dry_run else {"exit_code": 2}),
|
|
99
|
+
}
|
|
100
|
+
process = subprocess.run([str(path)], check=False, text=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, timeout=300)
|
|
101
|
+
return {
|
|
102
|
+
"kind": "local-script-run",
|
|
103
|
+
"schema_version": LOCAL_ARTIFACT_SCHEMA_VERSION,
|
|
104
|
+
"status": "ok" if process.returncode == 0 else "failed",
|
|
105
|
+
"ok": process.returncode == 0,
|
|
106
|
+
"id": item_id,
|
|
107
|
+
"path": str(path),
|
|
108
|
+
"exit_code": process.returncode,
|
|
109
|
+
"stdout": process.stdout[-4000:],
|
|
110
|
+
"stderr": process.stderr[-4000:],
|
|
111
|
+
"write_policy": "local_write",
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def local_agent_create(agent_id: str | None, *, description: str | None = None, force: bool = False) -> dict[str, Any]:
|
|
116
|
+
item_id = require_id(agent_id, "agent id")
|
|
117
|
+
path = local_home("agents") / item_id
|
|
118
|
+
if path.exists() and not force:
|
|
119
|
+
raise DevKitError(f"local agent already exists: {item_id}")
|
|
120
|
+
(path / "capabilities").mkdir(parents=True, exist_ok=True)
|
|
121
|
+
(path / "knowledge").mkdir(parents=True, exist_ok=True)
|
|
122
|
+
(path / "templates").mkdir(parents=True, exist_ok=True)
|
|
123
|
+
(path / "infra").mkdir(parents=True, exist_ok=True)
|
|
124
|
+
(path / "agent.yaml").write_text(render_agent_yaml(item_id, description), encoding="utf-8")
|
|
125
|
+
(path / "README.md").write_text(f"# {item_id}\n\n{description or 'Local Agent DevKit agent.'}\n", encoding="utf-8")
|
|
126
|
+
return local_artifact_payload("local-agent", "created", item_id, path, write_policy="local_config_write")
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def local_agent_validate(agent_id: str | None) -> dict[str, Any]:
|
|
130
|
+
item_id = require_id(agent_id, "agent id")
|
|
131
|
+
path = local_home("agents") / item_id
|
|
132
|
+
checks = [
|
|
133
|
+
{"id": "path-exists", "status": "passed" if path.exists() else "failed"},
|
|
134
|
+
{"id": "agent-yaml", "status": "passed" if (path / "agent.yaml").exists() else "failed"},
|
|
135
|
+
{"id": "capabilities-dir", "status": "passed" if (path / "capabilities").is_dir() else "failed"},
|
|
136
|
+
{"id": "knowledge-dir", "status": "passed" if (path / "knowledge").is_dir() else "failed"},
|
|
137
|
+
]
|
|
138
|
+
return {
|
|
139
|
+
"kind": "local-agent-validation",
|
|
140
|
+
"schema_version": LOCAL_ARTIFACT_SCHEMA_VERSION,
|
|
141
|
+
"status": "passed" if all(check["status"] == "passed" for check in checks) else "failed",
|
|
142
|
+
"id": item_id,
|
|
143
|
+
"path": str(path),
|
|
144
|
+
"checks": checks,
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def local_agent_show(agent_id: str | None) -> dict[str, Any]:
|
|
149
|
+
item_id = require_id(agent_id, "agent id")
|
|
150
|
+
path = local_home("agents") / item_id
|
|
151
|
+
if not (path / "agent.yaml").exists():
|
|
152
|
+
raise DevKitError(f"local agent not found: {item_id}")
|
|
153
|
+
payload = local_artifact_payload("local-agent", "ok", item_id, path, write_policy="read_only")
|
|
154
|
+
payload["manifest"] = (path / "agent.yaml").read_text(encoding="utf-8")
|
|
155
|
+
readme = path / "README.md"
|
|
156
|
+
if readme.exists():
|
|
157
|
+
payload["readme"] = readme.read_text(encoding="utf-8")
|
|
158
|
+
return payload
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def local_agent_list() -> dict[str, Any]:
|
|
162
|
+
return local_artifact_list("local-agents", "agents", marker="agent.yaml")
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def local_automation_create(
|
|
166
|
+
automation_id: str | None,
|
|
167
|
+
*,
|
|
168
|
+
title: str | None = None,
|
|
169
|
+
prompt: str | None = None,
|
|
170
|
+
command: str | None = None,
|
|
171
|
+
every: str | None = None,
|
|
172
|
+
cron: str | None = None,
|
|
173
|
+
force: bool = False,
|
|
174
|
+
) -> dict[str, Any]:
|
|
175
|
+
item_id = generated_id(automation_id, title, prefix="automation")
|
|
176
|
+
path = local_home("automations") / item_id
|
|
177
|
+
manifest = path / "automation.json"
|
|
178
|
+
if manifest.exists() and not force:
|
|
179
|
+
raise DevKitError(f"local automation already exists: {item_id}")
|
|
180
|
+
path.mkdir(parents=True, exist_ok=True)
|
|
181
|
+
payload = automation_manifest(item_id, title=title, prompt=prompt, command=command, every=every, cron=cron, enabled=True)
|
|
182
|
+
write_json(manifest, payload)
|
|
183
|
+
if command:
|
|
184
|
+
script_path = path / "run.sh"
|
|
185
|
+
script_path.write_text(render_script(command), encoding="utf-8")
|
|
186
|
+
script_path.chmod(0o755)
|
|
187
|
+
return local_automation_payload("created", item_id, path, payload)
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def local_automation_list() -> dict[str, Any]:
|
|
191
|
+
home = local_home("automations")
|
|
192
|
+
items = []
|
|
193
|
+
for manifest in sorted(home.glob("*/automation.json")):
|
|
194
|
+
payload = read_json(manifest)
|
|
195
|
+
item_id = str(payload.get("id") or manifest.parent.name)
|
|
196
|
+
items.append(
|
|
197
|
+
{
|
|
198
|
+
"id": item_id,
|
|
199
|
+
"kind": "local-automation",
|
|
200
|
+
"path": str(manifest.parent),
|
|
201
|
+
"enabled": payload.get("enabled") is True,
|
|
202
|
+
"title": payload.get("title"),
|
|
203
|
+
"schedule": payload.get("schedule") or {},
|
|
204
|
+
"write_policy": payload.get("write_policy") or "local_write",
|
|
205
|
+
}
|
|
206
|
+
)
|
|
207
|
+
return {"kind": "local-automations", "schema_version": LOCAL_ARTIFACT_SCHEMA_VERSION, "status": "ok", "home": str(home), "items": items}
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def local_automation_show(automation_id: str | None) -> dict[str, Any]:
|
|
211
|
+
item_id = require_id(automation_id, "automation id")
|
|
212
|
+
path = local_home("automations") / item_id
|
|
213
|
+
payload = read_required_automation(path, item_id)
|
|
214
|
+
return local_automation_payload("ok", item_id, path, payload)
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def local_automation_update(
|
|
218
|
+
automation_id: str | None,
|
|
219
|
+
*,
|
|
220
|
+
title: str | None = None,
|
|
221
|
+
prompt: str | None = None,
|
|
222
|
+
command: str | None = None,
|
|
223
|
+
every: str | None = None,
|
|
224
|
+
cron: str | None = None,
|
|
225
|
+
) -> dict[str, Any]:
|
|
226
|
+
item_id = require_id(automation_id, "automation id")
|
|
227
|
+
path = local_home("automations") / item_id
|
|
228
|
+
current = read_required_automation(path, item_id)
|
|
229
|
+
current_schedule = current.get("schedule") if isinstance(current.get("schedule"), dict) else {"type": "manual"}
|
|
230
|
+
updated = automation_manifest(
|
|
231
|
+
item_id,
|
|
232
|
+
title=title if title is not None else str(current.get("title") or ""),
|
|
233
|
+
prompt=prompt if prompt is not None else current.get("prompt"),
|
|
234
|
+
command=command if command is not None else current.get("command"),
|
|
235
|
+
every=every,
|
|
236
|
+
cron=cron,
|
|
237
|
+
enabled=current.get("enabled") is not False,
|
|
238
|
+
created_at=str(current.get("created_at") or now_iso()),
|
|
239
|
+
)
|
|
240
|
+
if every is None and cron is None:
|
|
241
|
+
updated["schedule"] = current_schedule
|
|
242
|
+
write_json(path / "automation.json", updated)
|
|
243
|
+
return local_automation_payload("updated", item_id, path, updated)
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def local_automation_enable(automation_id: str | None, enabled: bool) -> dict[str, Any]:
|
|
247
|
+
item_id = require_id(automation_id, "automation id")
|
|
248
|
+
path = local_home("automations") / item_id
|
|
249
|
+
payload = read_required_automation(path, item_id)
|
|
250
|
+
payload["enabled"] = enabled
|
|
251
|
+
payload["updated_at"] = now_iso()
|
|
252
|
+
write_json(path / "automation.json", payload)
|
|
253
|
+
return local_automation_payload("enabled" if enabled else "disabled", item_id, path, payload)
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def local_automation_remove(automation_id: str | None, *, yes: bool = False) -> dict[str, Any]:
|
|
257
|
+
item_id = require_id(automation_id, "automation id")
|
|
258
|
+
path = local_home("automations") / item_id
|
|
259
|
+
if not yes:
|
|
260
|
+
payload = local_artifact_payload("local-automation", "needs-confirmation", item_id, path, write_policy="local_config_write")
|
|
261
|
+
payload["ok"] = False
|
|
262
|
+
payload["exit_code"] = 2
|
|
263
|
+
return payload
|
|
264
|
+
shutil.rmtree(path, ignore_errors=True)
|
|
265
|
+
return local_artifact_payload("local-automation", "removed", item_id, path, write_policy="local_config_write")
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def local_automation_validate(automation_id: str | None) -> dict[str, Any]:
|
|
269
|
+
item_id = require_id(automation_id, "automation id")
|
|
270
|
+
path = local_home("automations") / item_id
|
|
271
|
+
payload = read_json(path / "automation.json") if (path / "automation.json").exists() else {}
|
|
272
|
+
checks = [
|
|
273
|
+
{"id": "path-exists", "status": "passed" if path.exists() else "failed"},
|
|
274
|
+
{"id": "manifest-exists", "status": "passed" if (path / "automation.json").exists() else "failed"},
|
|
275
|
+
{"id": "has-action", "status": "passed" if payload.get("prompt") or payload.get("command") else "failed"},
|
|
276
|
+
{"id": "no-stored-secret", "status": "passed" if not contains_secret_like_text(json.dumps(payload, ensure_ascii=False)) else "failed"},
|
|
277
|
+
]
|
|
278
|
+
return {
|
|
279
|
+
"kind": "local-automation-validation",
|
|
280
|
+
"schema_version": LOCAL_ARTIFACT_SCHEMA_VERSION,
|
|
281
|
+
"status": "passed" if all(check["status"] == "passed" for check in checks) else "failed",
|
|
282
|
+
"id": item_id,
|
|
283
|
+
"path": str(path),
|
|
284
|
+
"checks": checks,
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def local_home(*parts: str) -> Path:
|
|
289
|
+
ensure_app_home()
|
|
290
|
+
path = app_home() / "local" / Path(*parts)
|
|
291
|
+
path.mkdir(parents=True, exist_ok=True)
|
|
292
|
+
return path
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def local_artifact_list(kind: str, folder: str, *, marker: str) -> dict[str, Any]:
|
|
296
|
+
home = local_home(folder)
|
|
297
|
+
items = [
|
|
298
|
+
local_artifact_item(path.parent.name, path.parent, kind.removesuffix("s"), enabled=True)
|
|
299
|
+
for path in sorted(home.glob(f"*/{marker}"))
|
|
300
|
+
]
|
|
301
|
+
return {"kind": kind, "schema_version": LOCAL_ARTIFACT_SCHEMA_VERSION, "status": "ok", "home": str(home), "items": items}
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def local_artifact_show(kind: str, folder: str, item_id: str, *, marker: str) -> dict[str, Any]:
|
|
305
|
+
path = local_home(folder) / item_id
|
|
306
|
+
marker_path = path / marker
|
|
307
|
+
if not marker_path.exists():
|
|
308
|
+
raise DevKitError(f"{kind} not found: {item_id}")
|
|
309
|
+
payload = local_artifact_payload(kind, "ok", item_id, path, write_policy="read_only")
|
|
310
|
+
payload["content"] = marker_path.read_text(encoding="utf-8")
|
|
311
|
+
return payload
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def local_artifact_payload(kind: str, status: str, item_id: str, path: Path, *, write_policy: str) -> dict[str, Any]:
|
|
315
|
+
return {
|
|
316
|
+
"kind": kind,
|
|
317
|
+
"schema_version": LOCAL_ARTIFACT_SCHEMA_VERSION,
|
|
318
|
+
"status": status,
|
|
319
|
+
"id": item_id,
|
|
320
|
+
"path": str(path),
|
|
321
|
+
"write_policy": write_policy,
|
|
322
|
+
"stored_secret": False,
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def local_artifact_item(item_id: str, path: Path, kind: str, *, enabled: bool) -> dict[str, Any]:
|
|
327
|
+
return {"id": item_id, "kind": kind, "path": str(path), "enabled": enabled}
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
def require_id(value: str | None, label: str) -> str:
|
|
331
|
+
item_id = (value or "").strip()
|
|
332
|
+
if not ID_PATTERN.fullmatch(item_id):
|
|
333
|
+
raise DevKitError(f"{label} must use lowercase letters, numbers, dots, dashes or underscores")
|
|
334
|
+
return item_id
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
def generated_id(value: str | None, title: str | None, *, prefix: str) -> str:
|
|
338
|
+
if value:
|
|
339
|
+
return require_id(value, f"{prefix} id")
|
|
340
|
+
raw = title or f"{prefix}-{datetime.now(timezone.utc).strftime('%Y%m%d%H%M%S')}"
|
|
341
|
+
slug = re.sub(r"[^a-z0-9._-]+", "-", raw.strip().lower()).strip("-._")
|
|
342
|
+
return require_id(slug or prefix, f"{prefix} id")
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def automation_manifest(
|
|
346
|
+
item_id: str,
|
|
347
|
+
*,
|
|
348
|
+
title: str | None,
|
|
349
|
+
prompt: Any,
|
|
350
|
+
command: Any,
|
|
351
|
+
every: str | None,
|
|
352
|
+
cron: str | None,
|
|
353
|
+
enabled: bool,
|
|
354
|
+
created_at: str | None = None,
|
|
355
|
+
) -> dict[str, Any]:
|
|
356
|
+
schedule: dict[str, Any] = {"type": "manual"}
|
|
357
|
+
if every:
|
|
358
|
+
schedule = {"type": "interval", "every": every}
|
|
359
|
+
if cron:
|
|
360
|
+
schedule = {"type": "cron", "cron": cron}
|
|
361
|
+
now = now_iso()
|
|
362
|
+
return {
|
|
363
|
+
"schema_version": LOCAL_ARTIFACT_SCHEMA_VERSION,
|
|
364
|
+
"id": item_id,
|
|
365
|
+
"kind": "local-automation",
|
|
366
|
+
"title": title or item_id,
|
|
367
|
+
"prompt": prompt,
|
|
368
|
+
"command": command,
|
|
369
|
+
"schedule": schedule,
|
|
370
|
+
"enabled": enabled,
|
|
371
|
+
"write_policy": "local_write" if command else "local_config_write",
|
|
372
|
+
"external_writes": False,
|
|
373
|
+
"stored_secret": False,
|
|
374
|
+
"created_at": created_at or now,
|
|
375
|
+
"updated_at": now,
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
def local_automation_payload(status: str, item_id: str, path: Path, manifest: dict[str, Any]) -> dict[str, Any]:
|
|
380
|
+
payload = local_artifact_payload("local-automation", status, item_id, path, write_policy=str(manifest.get("write_policy") or "local_config_write"))
|
|
381
|
+
payload["automation"] = manifest
|
|
382
|
+
return payload
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
def read_required_automation(path: Path, item_id: str) -> dict[str, Any]:
|
|
386
|
+
manifest = path / "automation.json"
|
|
387
|
+
if not manifest.exists():
|
|
388
|
+
raise DevKitError(f"local automation not found: {item_id}")
|
|
389
|
+
return read_json(manifest)
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
def read_json(path: Path) -> dict[str, Any]:
|
|
393
|
+
try:
|
|
394
|
+
payload = json.loads(path.read_text(encoding="utf-8"))
|
|
395
|
+
except (OSError, json.JSONDecodeError):
|
|
396
|
+
return {}
|
|
397
|
+
return payload if isinstance(payload, dict) else {}
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
def write_json(path: Path, payload: dict[str, Any]) -> None:
|
|
401
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
402
|
+
path.write_text(json.dumps(payload, ensure_ascii=False, indent=2, sort_keys=True) + "\n", encoding="utf-8")
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
def contains_secret_like_text(text: str) -> bool:
|
|
406
|
+
return bool(re.search(r"(?i)(api[_-]?key|token|secret|password|senha|pat)\s*[:=]", text))
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
def now_iso() -> str:
|
|
410
|
+
return datetime.now(timezone.utc).isoformat()
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
def render_skill(skill_id: str, description: str | None) -> str:
|
|
414
|
+
return f"""---
|
|
415
|
+
name: {skill_id}
|
|
416
|
+
description: {description or 'Local Agent DevKit skill.'}
|
|
417
|
+
---
|
|
418
|
+
|
|
419
|
+
# {skill_id}
|
|
420
|
+
|
|
421
|
+
{description or 'Local skill created by Agent DevKit.'}
|
|
422
|
+
"""
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
def render_script(command: str | None) -> str:
|
|
426
|
+
body = command or "echo 'local script placeholder'"
|
|
427
|
+
return f"#!/usr/bin/env sh\nset -eu\n{body}\n"
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
def render_agent_yaml(agent_id: str, description: str | None) -> str:
|
|
431
|
+
return json.dumps(
|
|
432
|
+
{
|
|
433
|
+
"id": agent_id,
|
|
434
|
+
"kind": "agent",
|
|
435
|
+
"name": agent_id,
|
|
436
|
+
"version": "0.1.0",
|
|
437
|
+
"status": "draft",
|
|
438
|
+
"purpose": description or "Local Agent DevKit agent.",
|
|
439
|
+
"default_context": ["knowledge/context.md"],
|
|
440
|
+
"capabilities": [],
|
|
441
|
+
},
|
|
442
|
+
ensure_ascii=False,
|
|
443
|
+
indent=2,
|
|
444
|
+
) + "\n"
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
"""Canonical local-LLM CLI surface over Ollama and mini-brain policy."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import shutil
|
|
6
|
+
import subprocess
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from cli.aikit.mini_brain import DEFAULT_OLLAMA_MODEL, mini_brain_contract
|
|
10
|
+
from cli.aikit.model_router import build_model_plan
|
|
11
|
+
from cli.aikit.ollama import ollama_models, ollama_pull, ollama_status
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
LOCAL_LLM_SCHEMA_VERSION = "agent-devkit.local-llm/v1"
|
|
15
|
+
LOCAL_WORKERS = (
|
|
16
|
+
("local-classifier", "intent classification and routing hints"),
|
|
17
|
+
("local-summarizer", "short summaries and extraction"),
|
|
18
|
+
("local-coder", "mechanical code drafts with coordinator review"),
|
|
19
|
+
("local-log-analyzer", "log clustering and evidence extraction"),
|
|
20
|
+
("local-sql-helper", "SQL draft assistance with review required"),
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def local_llm_list() -> dict[str, Any]:
|
|
25
|
+
status = ollama_status()
|
|
26
|
+
contract = mini_brain_contract(ollama_payload=status)
|
|
27
|
+
return {
|
|
28
|
+
"kind": "local-llm",
|
|
29
|
+
"schema_version": LOCAL_LLM_SCHEMA_VERSION,
|
|
30
|
+
"status": "ok",
|
|
31
|
+
"provider": "ollama",
|
|
32
|
+
"mini_brain": contract,
|
|
33
|
+
"workers": [{"id": worker_id, "purpose": purpose} for worker_id, purpose in LOCAL_WORKERS],
|
|
34
|
+
"models": {
|
|
35
|
+
"status": status.get("status"),
|
|
36
|
+
"count": status.get("model_count", 0),
|
|
37
|
+
},
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def local_llm_doctor() -> dict[str, Any]:
|
|
42
|
+
status = ollama_status()
|
|
43
|
+
contract = mini_brain_contract(ollama_payload=status)
|
|
44
|
+
model_plan = build_model_plan("resuma estes logs operacionais")
|
|
45
|
+
ok = status.get("status") == "ok" and contract.get("enabled") is True
|
|
46
|
+
return {
|
|
47
|
+
"kind": "local-llm-doctor",
|
|
48
|
+
"schema_version": LOCAL_LLM_SCHEMA_VERSION,
|
|
49
|
+
"status": "ok" if ok else "partial",
|
|
50
|
+
"provider": "ollama",
|
|
51
|
+
"ollama": status,
|
|
52
|
+
"mini_brain": contract,
|
|
53
|
+
"model_plan": {
|
|
54
|
+
"strategy": model_plan.get("strategy"),
|
|
55
|
+
"local_llm_recommended": model_plan.get("local_llm_recommended"),
|
|
56
|
+
"local_llm_selected": model_plan.get("local_llm_selected"),
|
|
57
|
+
"local_llm_role": model_plan.get("local_llm_role"),
|
|
58
|
+
"forbidden_actions": model_plan.get("forbidden_actions") or [],
|
|
59
|
+
},
|
|
60
|
+
"next_steps": local_llm_next_steps(status=status, mini_brain=contract),
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def local_llm_models() -> dict[str, Any]:
|
|
65
|
+
payload = ollama_models()
|
|
66
|
+
payload["kind"] = "local-llm-models"
|
|
67
|
+
payload["schema_version"] = LOCAL_LLM_SCHEMA_VERSION
|
|
68
|
+
payload["provider"] = "ollama"
|
|
69
|
+
return payload
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def local_llm_install(model: str | None, *, dry_run: bool = False, yes: bool = False) -> dict[str, Any]:
|
|
73
|
+
model = model or DEFAULT_OLLAMA_MODEL
|
|
74
|
+
payload = ollama_pull(model, dry_run=dry_run, yes=yes)
|
|
75
|
+
payload["kind"] = "local-llm-install"
|
|
76
|
+
payload["schema_version"] = LOCAL_LLM_SCHEMA_VERSION
|
|
77
|
+
payload["provider"] = "ollama"
|
|
78
|
+
return payload
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def local_llm_remove(model: str | None, *, dry_run: bool = False, yes: bool = False) -> dict[str, Any]:
|
|
82
|
+
model = model or DEFAULT_OLLAMA_MODEL
|
|
83
|
+
command = ["ollama", "rm", model]
|
|
84
|
+
binary = shutil.which("ollama")
|
|
85
|
+
if dry_run or not yes:
|
|
86
|
+
needs_confirmation = not dry_run and not yes
|
|
87
|
+
return {
|
|
88
|
+
"kind": "local-llm-remove",
|
|
89
|
+
"schema_version": LOCAL_LLM_SCHEMA_VERSION,
|
|
90
|
+
"status": "planned" if dry_run else "needs-confirmation",
|
|
91
|
+
"ok": bool(dry_run),
|
|
92
|
+
"exit_code": 2 if needs_confirmation else 0,
|
|
93
|
+
"provider": "ollama",
|
|
94
|
+
"model": model,
|
|
95
|
+
"binary": binary,
|
|
96
|
+
"command": command,
|
|
97
|
+
"dry_run": dry_run,
|
|
98
|
+
"yes": yes,
|
|
99
|
+
"message": "Use --yes to remove the local model." if not dry_run and not yes else "Dry-run only.",
|
|
100
|
+
}
|
|
101
|
+
if not binary:
|
|
102
|
+
return {
|
|
103
|
+
"kind": "local-llm-remove",
|
|
104
|
+
"schema_version": LOCAL_LLM_SCHEMA_VERSION,
|
|
105
|
+
"status": "missing",
|
|
106
|
+
"ok": False,
|
|
107
|
+
"provider": "ollama",
|
|
108
|
+
"model": model,
|
|
109
|
+
"command": command,
|
|
110
|
+
"message": "Ollama binary not found in PATH.",
|
|
111
|
+
"exit_code": 2,
|
|
112
|
+
}
|
|
113
|
+
process = subprocess.run(command, check=False, text=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, timeout=120)
|
|
114
|
+
return {
|
|
115
|
+
"kind": "local-llm-remove",
|
|
116
|
+
"schema_version": LOCAL_LLM_SCHEMA_VERSION,
|
|
117
|
+
"status": "ok" if process.returncode == 0 else "failed",
|
|
118
|
+
"ok": process.returncode == 0,
|
|
119
|
+
"provider": "ollama",
|
|
120
|
+
"model": model,
|
|
121
|
+
"binary": binary,
|
|
122
|
+
"command": command,
|
|
123
|
+
"exit_code": process.returncode,
|
|
124
|
+
"stdout": process.stdout[-4000:],
|
|
125
|
+
"stderr": process.stderr[-4000:],
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def local_llm_benchmark(model: str | None = None) -> dict[str, Any]:
|
|
130
|
+
model = model or DEFAULT_OLLAMA_MODEL
|
|
131
|
+
status = ollama_status()
|
|
132
|
+
installed = {item.get("name") for item in status.get("models") or [] if isinstance(item, dict)}
|
|
133
|
+
available = status.get("status") == "ok" and model in installed
|
|
134
|
+
return {
|
|
135
|
+
"kind": "local-llm-benchmark",
|
|
136
|
+
"schema_version": LOCAL_LLM_SCHEMA_VERSION,
|
|
137
|
+
"status": "ready" if available else "planned",
|
|
138
|
+
"ok": available,
|
|
139
|
+
"provider": "ollama",
|
|
140
|
+
"model": model,
|
|
141
|
+
"checks": [
|
|
142
|
+
{"id": "ollama-binary", "status": "passed" if status.get("status") == "ok" else "failed"},
|
|
143
|
+
{"id": "model-installed", "status": "passed" if model in installed else "planned"},
|
|
144
|
+
{"id": "policy", "status": "passed", "message": "Benchmark is read-only and does not approve final work."},
|
|
145
|
+
],
|
|
146
|
+
"next_steps": [] if available else [f"agent local-llm install {model} --yes"],
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def local_llm_next_steps(*, status: dict[str, Any], mini_brain: dict[str, Any]) -> list[str]:
|
|
151
|
+
steps: list[str] = []
|
|
152
|
+
if status.get("status") != "ok":
|
|
153
|
+
install_plan = status.get("install_plan") if isinstance(status.get("install_plan"), dict) else {}
|
|
154
|
+
command = install_plan.get("command")
|
|
155
|
+
if command:
|
|
156
|
+
steps.append(f"Install Ollama: {command}")
|
|
157
|
+
if not mini_brain.get("enabled"):
|
|
158
|
+
steps.append("agent setup mini-brain --yes")
|
|
159
|
+
if status.get("model_count", 0) < 1:
|
|
160
|
+
steps.append(f"agent local-llm install {DEFAULT_OLLAMA_MODEL} --yes")
|
|
161
|
+
return steps
|
|
@@ -4,7 +4,9 @@
|
|
|
4
4
|
from __future__ import annotations
|
|
5
5
|
|
|
6
6
|
import json
|
|
7
|
+
import os
|
|
7
8
|
import sys
|
|
9
|
+
import traceback
|
|
8
10
|
|
|
9
11
|
from cli.aikit.cli_dispatch import dispatch, format_audit_warning, is_audit_warning, maybe_record_cli_audit
|
|
10
12
|
from cli.aikit.cli_parser import DETERMINISTIC_COMMANDS, LLM_COMMANDS, build_parser
|
|
@@ -26,6 +28,19 @@ def main(argv: list[str] | None = None, *, prog: str | None = None) -> int:
|
|
|
26
28
|
print(format_audit_warning(audit_result), file=sys.stderr)
|
|
27
29
|
print(f"error: {exc}", file=sys.stderr)
|
|
28
30
|
return 1
|
|
31
|
+
except Exception as exc: # noqa: BLE001 - CLI boundary must not leak tracebacks by default.
|
|
32
|
+
if os.environ.get("AI_DEVKIT_DEBUG") == "1":
|
|
33
|
+
traceback.print_exc(file=sys.stderr)
|
|
34
|
+
else:
|
|
35
|
+
print(
|
|
36
|
+
f"error: internal error: {type(exc).__name__}. "
|
|
37
|
+
"Run with AI_DEVKIT_DEBUG=1 for traceback.",
|
|
38
|
+
file=sys.stderr,
|
|
39
|
+
)
|
|
40
|
+
audit_result = maybe_record_cli_audit(args, result=None, error=f"{type(exc).__name__}: {exc}")
|
|
41
|
+
if is_audit_warning(audit_result):
|
|
42
|
+
print(format_audit_warning(audit_result), file=sys.stderr)
|
|
43
|
+
return 1
|
|
29
44
|
|
|
30
45
|
if result is None:
|
|
31
46
|
return 0
|