@agxnte/reidx 2.0.2
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 +724 -0
- package/bin/reidx.js +33 -0
- package/package.json +40 -0
- package/pyproject.toml +45 -0
- package/scripts/find-python.js +24 -0
- package/scripts/postinstall.js +35 -0
- package/src/reidx/__init__.py +3 -0
- package/src/reidx/__main__.py +9 -0
- package/src/reidx/__pycache__/__init__.cpython-312.pyc +0 -0
- package/src/reidx/app/__init__.py +3 -0
- package/src/reidx/app/__pycache__/__init__.cpython-312.pyc +0 -0
- package/src/reidx/app/__pycache__/commands.cpython-312.pyc +0 -0
- package/src/reidx/app/commands.py +221 -0
- package/src/reidx/automation/__init__.py +3 -0
- package/src/reidx/automation/__pycache__/__init__.cpython-312.pyc +0 -0
- package/src/reidx/automation/__pycache__/exec.cpython-312.pyc +0 -0
- package/src/reidx/automation/exec.py +35 -0
- package/src/reidx/config/__init__.py +10 -0
- package/src/reidx/config/__pycache__/__init__.cpython-312.pyc +0 -0
- package/src/reidx/config/__pycache__/loader.cpython-312.pyc +0 -0
- package/src/reidx/config/__pycache__/models.cpython-312.pyc +0 -0
- package/src/reidx/config/__pycache__/settings.cpython-312.pyc +0 -0
- package/src/reidx/config/loader.py +104 -0
- package/src/reidx/config/models.py +49 -0
- package/src/reidx/config/settings.py +127 -0
- package/src/reidx/deepreid/__init__.py +8 -0
- package/src/reidx/deepreid/__pycache__/__init__.cpython-312.pyc +0 -0
- package/src/reidx/deepreid/__pycache__/pipeline.cpython-312.pyc +0 -0
- package/src/reidx/deepreid/pipeline.py +221 -0
- package/src/reidx/diagnostics/__init__.py +0 -0
- package/src/reidx/diagnostics/__pycache__/__init__.cpython-312.pyc +0 -0
- package/src/reidx/diagnostics/__pycache__/logger.cpython-312.pyc +0 -0
- package/src/reidx/diagnostics/logger.py +56 -0
- package/src/reidx/goals/__init__.py +21 -0
- package/src/reidx/goals/__pycache__/__init__.cpython-312.pyc +0 -0
- package/src/reidx/goals/__pycache__/models.cpython-312.pyc +0 -0
- package/src/reidx/goals/__pycache__/store.cpython-312.pyc +0 -0
- package/src/reidx/goals/models.py +101 -0
- package/src/reidx/goals/store.py +233 -0
- package/src/reidx/integrations/__init__.py +3 -0
- package/src/reidx/integrations/mcp.py +70 -0
- package/src/reidx/nyx/__init__.py +3 -0
- package/src/reidx/nyx/__pycache__/__init__.cpython-312.pyc +0 -0
- package/src/reidx/nyx/__pycache__/persona.cpython-312.pyc +0 -0
- package/src/reidx/nyx/persona.py +35 -0
- package/src/reidx/policy/__init__.py +15 -0
- package/src/reidx/policy/__pycache__/__init__.cpython-312.pyc +0 -0
- package/src/reidx/policy/__pycache__/engine.cpython-312.pyc +0 -0
- package/src/reidx/policy/__pycache__/models.cpython-312.pyc +0 -0
- package/src/reidx/policy/engine.py +101 -0
- package/src/reidx/policy/models.py +35 -0
- package/src/reidx/provider/__init__.py +16 -0
- package/src/reidx/provider/__pycache__/__init__.cpython-312.pyc +0 -0
- package/src/reidx/provider/__pycache__/_http.cpython-312.pyc +0 -0
- package/src/reidx/provider/__pycache__/anthropic.cpython-312.pyc +0 -0
- package/src/reidx/provider/__pycache__/base.cpython-312.pyc +0 -0
- package/src/reidx/provider/__pycache__/ollama.cpython-312.pyc +0 -0
- package/src/reidx/provider/__pycache__/openai.cpython-312.pyc +0 -0
- package/src/reidx/provider/__pycache__/registry.cpython-312.pyc +0 -0
- package/src/reidx/provider/__pycache__/store.cpython-312.pyc +0 -0
- package/src/reidx/provider/__pycache__/stub.cpython-312.pyc +0 -0
- package/src/reidx/provider/_http.py +29 -0
- package/src/reidx/provider/anthropic.py +176 -0
- package/src/reidx/provider/base.py +50 -0
- package/src/reidx/provider/ollama.py +109 -0
- package/src/reidx/provider/openai.py +144 -0
- package/src/reidx/provider/registry.py +88 -0
- package/src/reidx/provider/store.py +141 -0
- package/src/reidx/provider/stub.py +60 -0
- package/src/reidx/provider_manager/__init__.py +40 -0
- package/src/reidx/provider_manager/__pycache__/__init__.cpython-312.pyc +0 -0
- package/src/reidx/provider_manager/__pycache__/catalog.cpython-312.pyc +0 -0
- package/src/reidx/provider_manager/__pycache__/database.cpython-312.pyc +0 -0
- package/src/reidx/provider_manager/__pycache__/keychain.cpython-312.pyc +0 -0
- package/src/reidx/provider_manager/__pycache__/palette.cpython-312.pyc +0 -0
- package/src/reidx/provider_manager/catalog.py +330 -0
- package/src/reidx/provider_manager/database.py +234 -0
- package/src/reidx/provider_manager/keychain.py +102 -0
- package/src/reidx/provider_manager/palette.py +831 -0
- package/src/reidx/runtime/__init__.py +5 -0
- package/src/reidx/runtime/__pycache__/__init__.cpython-312.pyc +0 -0
- package/src/reidx/runtime/__pycache__/agent.cpython-312.pyc +0 -0
- package/src/reidx/runtime/__pycache__/orchestrator.cpython-312.pyc +0 -0
- package/src/reidx/runtime/__pycache__/reasoning.cpython-312.pyc +0 -0
- package/src/reidx/runtime/__pycache__/state.cpython-312.pyc +0 -0
- package/src/reidx/runtime/__pycache__/subagent.cpython-312.pyc +0 -0
- package/src/reidx/runtime/agent.py +198 -0
- package/src/reidx/runtime/orchestrator.py +244 -0
- package/src/reidx/runtime/reasoning.py +77 -0
- package/src/reidx/runtime/state.py +32 -0
- package/src/reidx/runtime/subagent.py +174 -0
- package/src/reidx/session/__init__.py +4 -0
- package/src/reidx/session/__pycache__/__init__.cpython-312.pyc +0 -0
- package/src/reidx/session/__pycache__/models.cpython-312.pyc +0 -0
- package/src/reidx/session/__pycache__/store.cpython-312.pyc +0 -0
- package/src/reidx/session/models.py +43 -0
- package/src/reidx/session/store.py +101 -0
- package/src/reidx/tasks/__init__.py +4 -0
- package/src/reidx/tasks/__pycache__/__init__.cpython-312.pyc +0 -0
- package/src/reidx/tasks/__pycache__/models.cpython-312.pyc +0 -0
- package/src/reidx/tasks/__pycache__/store.cpython-312.pyc +0 -0
- package/src/reidx/tasks/models.py +44 -0
- package/src/reidx/tasks/store.py +86 -0
- package/src/reidx/tools/__init__.py +44 -0
- package/src/reidx/tools/__pycache__/__init__.cpython-312.pyc +0 -0
- package/src/reidx/tools/__pycache__/base.cpython-312.pyc +0 -0
- package/src/reidx/tools/__pycache__/file_tools.cpython-312.pyc +0 -0
- package/src/reidx/tools/__pycache__/registry.cpython-312.pyc +0 -0
- package/src/reidx/tools/__pycache__/shell_tool.cpython-312.pyc +0 -0
- package/src/reidx/tools/__pycache__/spawn_agent.cpython-312.pyc +0 -0
- package/src/reidx/tools/__pycache__/web_tools.cpython-312.pyc +0 -0
- package/src/reidx/tools/base.py +84 -0
- package/src/reidx/tools/file_tools.py +222 -0
- package/src/reidx/tools/registry.py +45 -0
- package/src/reidx/tools/shell_tool.py +75 -0
- package/src/reidx/tools/spawn_agent.py +185 -0
- package/src/reidx/tools/web_tools.py +216 -0
- package/src/reidx/ui/__init__.py +41 -0
- package/src/reidx/ui/__pycache__/__init__.cpython-312.pyc +0 -0
- package/src/reidx/ui/__pycache__/app.cpython-312.pyc +0 -0
- package/src/reidx/ui/__pycache__/commands.cpython-312.pyc +0 -0
- package/src/reidx/ui/__pycache__/render.cpython-312.pyc +0 -0
- package/src/reidx/ui/__pycache__/repl.cpython-312.pyc +0 -0
- package/src/reidx/ui/__pycache__/theme.cpython-312.pyc +0 -0
- package/src/reidx/ui/app.py +1343 -0
- package/src/reidx/ui/commands.py +663 -0
- package/src/reidx/ui/render.py +517 -0
- package/src/reidx/ui/repl.py +14 -0
- package/src/reidx/ui/theme.py +126 -0
- package/src/reidx/workflows/__init__.py +4 -0
- package/src/reidx/workflows/__pycache__/__init__.cpython-312.pyc +0 -0
- package/src/reidx/workflows/__pycache__/models.cpython-312.pyc +0 -0
- package/src/reidx/workflows/__pycache__/store.cpython-312.pyc +0 -0
- package/src/reidx/workflows/models.py +19 -0
- package/src/reidx/workflows/store.py +56 -0
package/bin/reidx.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
const { spawnSync } = require("node:child_process");
|
|
5
|
+
const { findPython } = require("../scripts/find-python");
|
|
6
|
+
|
|
7
|
+
function main() {
|
|
8
|
+
const python = findPython();
|
|
9
|
+
if (!python) {
|
|
10
|
+
console.error(
|
|
11
|
+
"reidx: no Python 3.12+ interpreter found on PATH.\n" +
|
|
12
|
+
"Install Python (https://www.python.org/downloads/) and re-run `npm install -g reidx`."
|
|
13
|
+
);
|
|
14
|
+
process.exit(1);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const result = spawnSync(
|
|
18
|
+
python.cmd,
|
|
19
|
+
[...python.args, "-m", "reidx", ...process.argv.slice(2)],
|
|
20
|
+
{ stdio: "inherit" }
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
if (result.error) {
|
|
24
|
+
console.error(
|
|
25
|
+
`reidx: failed to launch Python (${python.cmd}): ${result.error.message}`
|
|
26
|
+
);
|
|
27
|
+
process.exit(1);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
process.exit(result.status === null ? 1 : result.status);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
main();
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@agxnte/reidx",
|
|
3
|
+
"version": "2.0.2",
|
|
4
|
+
"description": "Terminal-native personal intelligence and coding CLI (agent-first runtime)",
|
|
5
|
+
"bin": {
|
|
6
|
+
"reid": "bin/reidx.js"
|
|
7
|
+
},
|
|
8
|
+
"scripts": {
|
|
9
|
+
"postinstall": "node scripts/postinstall.js"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"bin",
|
|
13
|
+
"scripts",
|
|
14
|
+
"src",
|
|
15
|
+
"pyproject.toml",
|
|
16
|
+
"README.md"
|
|
17
|
+
],
|
|
18
|
+
"engines": {
|
|
19
|
+
"node": ">=16"
|
|
20
|
+
},
|
|
21
|
+
"os": [
|
|
22
|
+
"win32",
|
|
23
|
+
"darwin",
|
|
24
|
+
"linux"
|
|
25
|
+
],
|
|
26
|
+
"preferGlobal": true,
|
|
27
|
+
"license": "MIT",
|
|
28
|
+
"author": "Reid",
|
|
29
|
+
"keywords": [
|
|
30
|
+
"cli",
|
|
31
|
+
"agent",
|
|
32
|
+
"ai",
|
|
33
|
+
"reidx",
|
|
34
|
+
"terminal"
|
|
35
|
+
],
|
|
36
|
+
"repository": {
|
|
37
|
+
"type": "git",
|
|
38
|
+
"url": "git+https://github.com/ryyReid/ReidX.git"
|
|
39
|
+
}
|
|
40
|
+
}
|
package/pyproject.toml
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "reidx"
|
|
3
|
+
version = "2.0.1"
|
|
4
|
+
description = "Terminal-native personal intelligence and coding CLI (agent-first runtime)"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.12"
|
|
7
|
+
license = { text = "MIT" }
|
|
8
|
+
authors = [{ name = "Reid" }]
|
|
9
|
+
dependencies = [
|
|
10
|
+
"typer>=0.12.0",
|
|
11
|
+
"pydantic>=2.7.0",
|
|
12
|
+
"rich>=13.7.0",
|
|
13
|
+
"prompt-toolkit>=3.0.0",
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
[project.optional-dependencies]
|
|
17
|
+
dev = [
|
|
18
|
+
"ruff>=0.6.0",
|
|
19
|
+
"pytest>=8.0.0",
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
[project.scripts]
|
|
23
|
+
reid = "reidx.app.commands:app"
|
|
24
|
+
|
|
25
|
+
[build-system]
|
|
26
|
+
requires = ["hatchling>=1.21"]
|
|
27
|
+
build-backend = "hatchling.build"
|
|
28
|
+
|
|
29
|
+
[tool.hatch.build.targets.wheel]
|
|
30
|
+
packages = ["src/reidx"]
|
|
31
|
+
|
|
32
|
+
[tool.ruff]
|
|
33
|
+
line-length = 100
|
|
34
|
+
target-version = "py312"
|
|
35
|
+
src = ["src"]
|
|
36
|
+
|
|
37
|
+
[tool.ruff.lint]
|
|
38
|
+
select = ["E", "F", "I", "UP", "B"]
|
|
39
|
+
ignore = ["E501"]
|
|
40
|
+
|
|
41
|
+
[tool.ruff.lint.flake8-bugbear]
|
|
42
|
+
extend-immutable-calls = ["typer.Option"]
|
|
43
|
+
|
|
44
|
+
[tool.ruff.lint.per-file-ignores]
|
|
45
|
+
"__init__.py" = ["F401"]
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const { spawnSync } = require("node:child_process");
|
|
4
|
+
|
|
5
|
+
const CANDIDATES =
|
|
6
|
+
process.platform === "win32"
|
|
7
|
+
? [["py", ["-3"]], ["python", []], ["python3", []]]
|
|
8
|
+
: [["python3", []], ["python", []]];
|
|
9
|
+
|
|
10
|
+
function commandWorks(cmd, args) {
|
|
11
|
+
const result = spawnSync(cmd, [...args, "--version"], { stdio: "ignore" });
|
|
12
|
+
return !result.error && result.status === 0;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function findPython() {
|
|
16
|
+
for (const [cmd, args] of CANDIDATES) {
|
|
17
|
+
if (commandWorks(cmd, args)) {
|
|
18
|
+
return { cmd, args };
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
module.exports = { findPython };
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const path = require("node:path");
|
|
4
|
+
const { spawnSync } = require("node:child_process");
|
|
5
|
+
const { findPython } = require("./find-python");
|
|
6
|
+
|
|
7
|
+
const PACKAGE_ROOT = path.join(__dirname, "..");
|
|
8
|
+
|
|
9
|
+
function pipInstall(python) {
|
|
10
|
+
const args = [...python.args, "-m", "pip", "install", "--user", "--quiet", PACKAGE_ROOT];
|
|
11
|
+
const result = spawnSync(python.cmd, args, { stdio: "inherit" });
|
|
12
|
+
return !result.error && result.status === 0;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function main() {
|
|
16
|
+
const python = findPython();
|
|
17
|
+
if (!python) {
|
|
18
|
+
console.warn(
|
|
19
|
+
"reidx: no Python 3.12+ interpreter found on PATH — skipping automatic setup.\n" +
|
|
20
|
+
"Install Python, then run: pip install --user " + JSON.stringify(PACKAGE_ROOT)
|
|
21
|
+
);
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
console.log("reidx: installing Python package (reidx) via pip...");
|
|
26
|
+
if (!pipInstall(python)) {
|
|
27
|
+
console.warn(
|
|
28
|
+
"reidx: automatic `pip install` failed.\n" +
|
|
29
|
+
"Run it manually: " +
|
|
30
|
+
`${python.cmd} ${python.args.join(" ")} -m pip install --user ${JSON.stringify(PACKAGE_ROOT)}`
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
main();
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
"""App layer: Typer command surface and dependency wiring.
|
|
2
|
+
|
|
3
|
+
This is the only place that composes ConfigLoader + ProviderRegistry + ToolRegistry
|
|
4
|
+
into an Orchestrator. Commands stay thin — they build an orchestrator and delegate
|
|
5
|
+
to runtime/ui/automation layers.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import sys
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
import typer
|
|
13
|
+
from rich.console import Console
|
|
14
|
+
|
|
15
|
+
from reidx import __version__
|
|
16
|
+
from reidx.automation.exec import exec_run
|
|
17
|
+
from reidx.config.loader import ConfigLoader
|
|
18
|
+
from reidx.config.models import Config
|
|
19
|
+
from reidx.deepreid import format_markdown, run_deepreid, save_deepreid_result
|
|
20
|
+
from reidx.diagnostics.logger import get_logger
|
|
21
|
+
from reidx.provider.registry import default_registry
|
|
22
|
+
from reidx.provider.store import load_into as load_stored_providers
|
|
23
|
+
from reidx.runtime.orchestrator import Orchestrator
|
|
24
|
+
from reidx.tools import default_registry as tools_registry
|
|
25
|
+
from reidx.ui import render
|
|
26
|
+
from reidx.ui.repl import repl
|
|
27
|
+
|
|
28
|
+
log = get_logger("reidx.app")
|
|
29
|
+
console = Console()
|
|
30
|
+
|
|
31
|
+
app = typer.Typer(
|
|
32
|
+
name="reid",
|
|
33
|
+
help="Terminal-native agent-first CLI runtime.",
|
|
34
|
+
no_args_is_help=False,
|
|
35
|
+
add_completion=False,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
_PROMPT_ARG_HELP = (
|
|
39
|
+
"Inject this as the first prompt right after launch — same as typing it into the "
|
|
40
|
+
"box and pressing Enter, except the session stays interactive afterward (unlike "
|
|
41
|
+
"`reid exec`, which runs once and exits). If omitted and stdin isn't a "
|
|
42
|
+
"terminal (piped input), stdin is read as the prompt instead."
|
|
43
|
+
)
|
|
44
|
+
_PROMPT_FILE_HELP = "Read the prompt from a text file instead of the command line (e.g. a long or multi-line prompt)."
|
|
45
|
+
_NYX_HELP = "Launch in Nyx mode: a redteam/offensive-security assistant persona for authorized pentesting and CTF work."
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _stdin_prompt() -> str | None:
|
|
49
|
+
if sys.stdin.isatty():
|
|
50
|
+
return None
|
|
51
|
+
data = sys.stdin.read().strip()
|
|
52
|
+
return data or None
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _resolve_prompt(prompt: str | None, prompt_file: Path | None, *, fall_back_to_stdin: bool) -> str | None:
|
|
56
|
+
if prompt and prompt_file:
|
|
57
|
+
render.print_error("pass either a prompt argument or --file, not both")
|
|
58
|
+
raise typer.Exit(code=1)
|
|
59
|
+
if prompt_file:
|
|
60
|
+
try:
|
|
61
|
+
text = prompt_file.read_text(encoding="utf-8").strip()
|
|
62
|
+
except OSError as exc:
|
|
63
|
+
render.print_error(f"failed to read prompt file {prompt_file}: {exc}")
|
|
64
|
+
raise typer.Exit(code=1) from exc
|
|
65
|
+
if not text:
|
|
66
|
+
render.print_error(f"prompt file is empty: {prompt_file}")
|
|
67
|
+
raise typer.Exit(code=1)
|
|
68
|
+
return text
|
|
69
|
+
if prompt:
|
|
70
|
+
return prompt
|
|
71
|
+
return _stdin_prompt() if fall_back_to_stdin else None
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@app.callback(invoke_without_command=True)
|
|
75
|
+
def _main(
|
|
76
|
+
ctx: typer.Context,
|
|
77
|
+
prompt_file: Path | None = typer.Option(None, "--file", "-f", help=_PROMPT_FILE_HELP),
|
|
78
|
+
nyx: bool = typer.Option(False, "--nyx", help=_NYX_HELP),
|
|
79
|
+
) -> None:
|
|
80
|
+
"""With no subcommand, launch interactive mode."""
|
|
81
|
+
# No positional prompt argument here on purpose: a Typer/Click group with
|
|
82
|
+
# both subcommands and its own positional argument resolves ambiguously —
|
|
83
|
+
# tested and confirmed it swallows subcommand names ("reid version",
|
|
84
|
+
# "reid sessions", ...) as the argument instead of dispatching to
|
|
85
|
+
# them. `--file`/`-f` is a named option, not positional, so it doesn't
|
|
86
|
+
# have that problem. `reid interactive "<prompt>"` is the unambiguous
|
|
87
|
+
# way to inject literal prompt text; bare `reid` still just launches
|
|
88
|
+
# empty (or reads piped stdin), as before.
|
|
89
|
+
if ctx.invoked_subcommand is None:
|
|
90
|
+
initial = _resolve_prompt(None, prompt_file, fall_back_to_stdin=True)
|
|
91
|
+
orch = build_orchestrator()
|
|
92
|
+
if nyx:
|
|
93
|
+
orch.set_nyx(True)
|
|
94
|
+
repl(orch, initial_prompt=initial)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def build_orchestrator(config: Config | None = None) -> Orchestrator:
|
|
98
|
+
cfg = config or ConfigLoader().load()
|
|
99
|
+
providers = default_registry(cfg)
|
|
100
|
+
load_stored_providers(providers, cfg.storage_root or (Path.home() / ".reidx"))
|
|
101
|
+
# `/connect` may have persisted a provider that isn't yet `cfg.default_provider`.
|
|
102
|
+
# We never auto-promote — stub stays default unless the user did `/use` last
|
|
103
|
+
# session (that's a session setting, not a config change). Fall back to stub
|
|
104
|
+
# if the configured default isn't registered.
|
|
105
|
+
default_name = cfg.default_provider if providers.has(cfg.default_provider) else "stub"
|
|
106
|
+
provider = providers.get(default_name)
|
|
107
|
+
return Orchestrator(cfg, provider, tools_registry(), providers=providers)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
@app.command()
|
|
111
|
+
def interactive(
|
|
112
|
+
prompt: str | None = typer.Argument(None, help=_PROMPT_ARG_HELP),
|
|
113
|
+
prompt_file: Path | None = typer.Option(None, "--file", "-f", help=_PROMPT_FILE_HELP),
|
|
114
|
+
nyx: bool = typer.Option(False, "--nyx", help=_NYX_HELP),
|
|
115
|
+
) -> None:
|
|
116
|
+
"""Launch interactive mode (default)."""
|
|
117
|
+
initial = _resolve_prompt(prompt, prompt_file, fall_back_to_stdin=True)
|
|
118
|
+
orch = build_orchestrator()
|
|
119
|
+
if nyx:
|
|
120
|
+
orch.set_nyx(True)
|
|
121
|
+
repl(orch, initial_prompt=initial)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
@app.command(name="exec")
|
|
125
|
+
def exec_(
|
|
126
|
+
prompt: str | None = typer.Argument(None, help="Prompt to run non-interactively."),
|
|
127
|
+
prompt_file: Path | None = typer.Option(None, "--file", "-f", help=_PROMPT_FILE_HELP),
|
|
128
|
+
nyx: bool = typer.Option(False, "--nyx", help=_NYX_HELP),
|
|
129
|
+
) -> None:
|
|
130
|
+
"""Run a single prompt non-interactively (headless)."""
|
|
131
|
+
resolved = _resolve_prompt(prompt, prompt_file, fall_back_to_stdin=False)
|
|
132
|
+
if not resolved:
|
|
133
|
+
render.print_error("usage: reid exec \"<prompt>\" (or --file <path>)")
|
|
134
|
+
raise typer.Exit(code=1)
|
|
135
|
+
orch = build_orchestrator()
|
|
136
|
+
if nyx:
|
|
137
|
+
orch.set_nyx(True)
|
|
138
|
+
raise typer.Exit(code=exec_run(orch, resolved))
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
@app.command()
|
|
142
|
+
def deepreid(
|
|
143
|
+
task: str | None = typer.Argument(None, help=_PROMPT_ARG_HELP),
|
|
144
|
+
prompt_file: Path | None = typer.Option(None, "--file", "-f", help=_PROMPT_FILE_HELP),
|
|
145
|
+
) -> None:
|
|
146
|
+
"""Plan + review a task via Researcher/Planner/Critic subagents (no code changes)."""
|
|
147
|
+
resolved = _resolve_prompt(task, prompt_file, fall_back_to_stdin=True)
|
|
148
|
+
if not resolved:
|
|
149
|
+
render.print_error('usage: reid deepreid "<task>" (or --file <path>)')
|
|
150
|
+
raise typer.Exit(code=1)
|
|
151
|
+
cfg = ConfigLoader().load()
|
|
152
|
+
provider = default_registry(cfg).get(cfg.default_provider)
|
|
153
|
+
result = run_deepreid(cfg, provider, Path.cwd(), resolved, on_progress=render.print_info)
|
|
154
|
+
path = save_deepreid_result(cfg, result)
|
|
155
|
+
console.print(format_markdown(result))
|
|
156
|
+
render.print_info(f"saved to {path}")
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
@app.command()
|
|
160
|
+
def resume(session_id: str = typer.Argument(..., help="Session id to resume.")) -> None:
|
|
161
|
+
"""Resume a prior session, then enter interactive mode."""
|
|
162
|
+
orch = build_orchestrator()
|
|
163
|
+
try:
|
|
164
|
+
orch.resume_session(session_id)
|
|
165
|
+
except KeyError as exc:
|
|
166
|
+
render.print_error(str(exc))
|
|
167
|
+
raise typer.Exit(code=1) from exc
|
|
168
|
+
render.print_info(f"resumed {session_id}")
|
|
169
|
+
repl(orch)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
@app.command()
|
|
173
|
+
def sessions() -> None:
|
|
174
|
+
"""List sessions."""
|
|
175
|
+
orch = build_orchestrator()
|
|
176
|
+
render.print_sessions(orch.session_store.list())
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
@app.command()
|
|
180
|
+
def config_show() -> None:
|
|
181
|
+
"""Show the effective (merged) configuration."""
|
|
182
|
+
cfg = ConfigLoader().load()
|
|
183
|
+
console.print(cfg.model_dump_json(indent=2, exclude={"providers": {"__all__": {"api_key"}}}))
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
@app.command()
|
|
187
|
+
def tools() -> None:
|
|
188
|
+
"""List registered tools."""
|
|
189
|
+
orch = build_orchestrator()
|
|
190
|
+
from reidx.ui.render import print_tools
|
|
191
|
+
|
|
192
|
+
print_tools(orch.tools.definitions())
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
@app.command()
|
|
196
|
+
def doctor() -> None:
|
|
197
|
+
"""Run environment diagnostics."""
|
|
198
|
+
cfg = ConfigLoader().load()
|
|
199
|
+
orch = build_orchestrator(cfg)
|
|
200
|
+
from reidx.config.settings import settings_path
|
|
201
|
+
|
|
202
|
+
sp = settings_path()
|
|
203
|
+
console.print(f"[bold]reid[/] {__version__}")
|
|
204
|
+
console.print(f"settings {sp} ({'found' if sp.exists() else 'missing'})")
|
|
205
|
+
console.print(f"python {sys.executable} ({sys.version.split()[0]})")
|
|
206
|
+
console.print(f"workspace {cfg.workspace_root}")
|
|
207
|
+
console.print(f"storage {cfg.storage_root}")
|
|
208
|
+
console.print(f"provider {cfg.default_provider}")
|
|
209
|
+
console.print(f"mode {cfg.policy.default_mode.value}")
|
|
210
|
+
console.print(f"providers {orch.provider.name} (active), {orch.tools.definitions().__len__()} tools")
|
|
211
|
+
import os
|
|
212
|
+
|
|
213
|
+
has_key = bool(os.environ.get("ANTHROPIC_API_KEY", "").strip())
|
|
214
|
+
console.print(f"anthropic {'configured' if has_key else 'not configured'} (env: ANTHROPIC_API_KEY)")
|
|
215
|
+
console.print("[green]ok[/] runtime importable; provider available")
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
@app.command()
|
|
219
|
+
def version() -> None:
|
|
220
|
+
"""Show version and runtime info."""
|
|
221
|
+
console.print(f"reid {__version__}")
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""Automation layer: non-interactive execution.
|
|
2
|
+
|
|
3
|
+
`exec_run` runs a single user prompt against a fresh session without entering the
|
|
4
|
+
REPL, prints the result, and exits with a meaningful code. Scheduled/background
|
|
5
|
+
work is TODO (roadmap Phase 7).
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import sys
|
|
10
|
+
|
|
11
|
+
from reidx.runtime.orchestrator import Orchestrator
|
|
12
|
+
from reidx.tools.base import Approver
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _auto_approve(prompt: str) -> bool:
|
|
16
|
+
"""Headless approver: auto-allows in autonomous mode, denies otherwise.
|
|
17
|
+
|
|
18
|
+
For exec mode the operator should pre-set permission mode via config/env.
|
|
19
|
+
Prompts in strict/balanced modes are denied to avoid hanging on stdin.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
# The orchestrator's policy mode is the source of truth; here we just allow
|
|
23
|
+
# because exec is expected to run with AUTONOMOUS configured. Denial would
|
|
24
|
+
# block every tool call silently, which is worse for a headless run.
|
|
25
|
+
del prompt
|
|
26
|
+
return True
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def exec_run(orchestrator: Orchestrator, prompt: str, approver: Approver | None = None) -> int:
|
|
30
|
+
orchestrator.start_session(title=f"exec: {prompt[:40]}")
|
|
31
|
+
result = orchestrator.submit_task(prompt, approver=approver or _auto_approve)
|
|
32
|
+
print(result["text"])
|
|
33
|
+
if result["tools"]:
|
|
34
|
+
print(f"\n[{len(result['tools'])} tool call(s)]", file=sys.stderr)
|
|
35
|
+
return 0 if result["text"] else 1
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"""Config loader: merge global + project + env into a validated Config.
|
|
2
|
+
|
|
3
|
+
Precedence (low -> high): defaults < global (~/.reidx/config.json)
|
|
4
|
+
< project (./.reidx/config.json) < environment overrides.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
import os
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
from reidx.config.models import Config, default_config
|
|
13
|
+
from reidx.config.settings import apply_settings_env, read_reidx_block
|
|
14
|
+
from reidx.diagnostics.logger import get_logger
|
|
15
|
+
|
|
16
|
+
log = get_logger("reidx.config")
|
|
17
|
+
|
|
18
|
+
GLOBAL_DIR = Path.home() / ".reidx"
|
|
19
|
+
PROJECT_DIR = Path(".reidx")
|
|
20
|
+
CONFIG_FILENAME = "config.json"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _read_json(path: Path) -> dict:
|
|
24
|
+
if not path.exists():
|
|
25
|
+
return {}
|
|
26
|
+
try:
|
|
27
|
+
return json.loads(path.read_text(encoding="utf-8"))
|
|
28
|
+
except (json.JSONDecodeError, OSError) as exc:
|
|
29
|
+
log.warning("failed to read config %s: %s", path, exc)
|
|
30
|
+
return {}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _env_overrides() -> dict:
|
|
34
|
+
overrides: dict = {}
|
|
35
|
+
provider = os.environ.get("REIDX_PROVIDER")
|
|
36
|
+
if provider:
|
|
37
|
+
overrides["default_provider"] = provider
|
|
38
|
+
workspace = os.environ.get("REIDX_WORKSPACE")
|
|
39
|
+
if workspace:
|
|
40
|
+
overrides["workspace_root"] = workspace
|
|
41
|
+
storage = os.environ.get("REIDX_STORAGE")
|
|
42
|
+
if storage:
|
|
43
|
+
overrides["storage_root"] = storage
|
|
44
|
+
mode = os.environ.get("REIDX_PERMISSION_MODE")
|
|
45
|
+
if mode:
|
|
46
|
+
overrides.setdefault("policy", {})["default_mode"] = mode
|
|
47
|
+
log_level = os.environ.get("REIDX_LOG_LEVEL")
|
|
48
|
+
if log_level:
|
|
49
|
+
overrides["log_level"] = log_level
|
|
50
|
+
return overrides
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _deep_merge(base: dict, over: dict) -> dict:
|
|
54
|
+
out = dict(base)
|
|
55
|
+
for key, value in over.items():
|
|
56
|
+
if isinstance(value, dict) and isinstance(out.get(key), dict):
|
|
57
|
+
out[key] = _deep_merge(out[key], value)
|
|
58
|
+
else:
|
|
59
|
+
out[key] = value
|
|
60
|
+
return out
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class ConfigLoader:
|
|
64
|
+
"""Loads and persists config across global + project scopes."""
|
|
65
|
+
|
|
66
|
+
def __init__(self, global_dir: Path = GLOBAL_DIR, project_dir: Path = PROJECT_DIR) -> None:
|
|
67
|
+
self.global_dir = global_dir
|
|
68
|
+
self.project_dir = project_dir
|
|
69
|
+
|
|
70
|
+
def load(self) -> Config:
|
|
71
|
+
# Apply the Reidchat settings file's env block (ANTHROPIC_* creds) before
|
|
72
|
+
# anything reads the environment, so the provider routes through Reidchat.
|
|
73
|
+
apply_settings_env()
|
|
74
|
+
data = default_config().model_dump(mode="json")
|
|
75
|
+
data = _deep_merge(data, _read_json(self.global_dir / CONFIG_FILENAME))
|
|
76
|
+
data = _deep_merge(data, _read_json(self.project_dir / CONFIG_FILENAME))
|
|
77
|
+
# Claude-Code-shaped settings.json (`reidx` block) — sits above
|
|
78
|
+
# .reidx/config.json so the project's baked-in settings file wins
|
|
79
|
+
# over an older on-disk config; env vars still win over both.
|
|
80
|
+
data = _deep_merge(data, read_reidx_block())
|
|
81
|
+
data = _deep_merge(data, _env_overrides())
|
|
82
|
+
|
|
83
|
+
cfg = Config.model_validate(data)
|
|
84
|
+
if cfg.storage_root is None:
|
|
85
|
+
cfg.storage_root = self.global_dir
|
|
86
|
+
if cfg.workspace_root is None:
|
|
87
|
+
cfg.workspace_root = Path.cwd().resolve()
|
|
88
|
+
return cfg
|
|
89
|
+
|
|
90
|
+
def save_global(self, cfg: Config) -> None:
|
|
91
|
+
self.global_dir.mkdir(parents=True, exist_ok=True)
|
|
92
|
+
path = self.global_dir / CONFIG_FILENAME
|
|
93
|
+
path.write_text(
|
|
94
|
+
cfg.model_dump_json(indent=2, exclude_none=True, mode="json"),
|
|
95
|
+
encoding="utf-8",
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
def save_project(self, cfg: Config) -> None:
|
|
99
|
+
self.project_dir.mkdir(parents=True, exist_ok=True)
|
|
100
|
+
path = self.project_dir / CONFIG_FILENAME
|
|
101
|
+
path.write_text(
|
|
102
|
+
cfg.model_dump_json(indent=2, exclude_none=True, mode="json"),
|
|
103
|
+
encoding="utf-8",
|
|
104
|
+
)
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""Config models: provider, policy, and top-level Config schemas.
|
|
2
|
+
|
|
3
|
+
Pydantic v2 models. `Config` is the merged, validated runtime configuration.
|
|
4
|
+
"""
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from pydantic import BaseModel, Field, SecretStr
|
|
10
|
+
|
|
11
|
+
from reidx.policy.models import PermissionMode
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ProviderConfig(BaseModel):
|
|
15
|
+
name: str
|
|
16
|
+
# Which client to build (anthropic/openai/openai-compatible/ollama). Empty
|
|
17
|
+
# means "same as name" — fine for the built-in kinds whose name matches.
|
|
18
|
+
kind: str = ""
|
|
19
|
+
base_url: str | None = None
|
|
20
|
+
api_key: SecretStr | None = None
|
|
21
|
+
default_model: str = ""
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class PolicyConfig(BaseModel):
|
|
25
|
+
default_mode: PermissionMode = PermissionMode.BALANCED
|
|
26
|
+
allowed_commands: list[str] = Field(default_factory=list)
|
|
27
|
+
blocked_commands: list[str] = Field(default_factory=list)
|
|
28
|
+
additional_writable_roots: list[Path] = Field(default_factory=list)
|
|
29
|
+
read_only_paths: list[Path] = Field(default_factory=list)
|
|
30
|
+
shell_timeout_seconds: int = 30
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class Config(BaseModel):
|
|
34
|
+
providers: dict[str, ProviderConfig] = Field(default_factory=dict)
|
|
35
|
+
default_provider: str = "stub"
|
|
36
|
+
policy: PolicyConfig = Field(default_factory=PolicyConfig)
|
|
37
|
+
workspace_root: Path | None = None
|
|
38
|
+
storage_root: Path | None = None # defaults to ~/.reidx when loaded
|
|
39
|
+
log_level: str = "INFO"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def default_config() -> Config:
|
|
43
|
+
"""Baseline config with a stub provider so the runtime is exercisable without API keys."""
|
|
44
|
+
return Config(
|
|
45
|
+
providers={
|
|
46
|
+
"stub": ProviderConfig(name="stub", default_model="stub-v0"),
|
|
47
|
+
},
|
|
48
|
+
default_provider="stub",
|
|
49
|
+
)
|