@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.
Files changed (135) hide show
  1. package/README.md +724 -0
  2. package/bin/reidx.js +33 -0
  3. package/package.json +40 -0
  4. package/pyproject.toml +45 -0
  5. package/scripts/find-python.js +24 -0
  6. package/scripts/postinstall.js +35 -0
  7. package/src/reidx/__init__.py +3 -0
  8. package/src/reidx/__main__.py +9 -0
  9. package/src/reidx/__pycache__/__init__.cpython-312.pyc +0 -0
  10. package/src/reidx/app/__init__.py +3 -0
  11. package/src/reidx/app/__pycache__/__init__.cpython-312.pyc +0 -0
  12. package/src/reidx/app/__pycache__/commands.cpython-312.pyc +0 -0
  13. package/src/reidx/app/commands.py +221 -0
  14. package/src/reidx/automation/__init__.py +3 -0
  15. package/src/reidx/automation/__pycache__/__init__.cpython-312.pyc +0 -0
  16. package/src/reidx/automation/__pycache__/exec.cpython-312.pyc +0 -0
  17. package/src/reidx/automation/exec.py +35 -0
  18. package/src/reidx/config/__init__.py +10 -0
  19. package/src/reidx/config/__pycache__/__init__.cpython-312.pyc +0 -0
  20. package/src/reidx/config/__pycache__/loader.cpython-312.pyc +0 -0
  21. package/src/reidx/config/__pycache__/models.cpython-312.pyc +0 -0
  22. package/src/reidx/config/__pycache__/settings.cpython-312.pyc +0 -0
  23. package/src/reidx/config/loader.py +104 -0
  24. package/src/reidx/config/models.py +49 -0
  25. package/src/reidx/config/settings.py +127 -0
  26. package/src/reidx/deepreid/__init__.py +8 -0
  27. package/src/reidx/deepreid/__pycache__/__init__.cpython-312.pyc +0 -0
  28. package/src/reidx/deepreid/__pycache__/pipeline.cpython-312.pyc +0 -0
  29. package/src/reidx/deepreid/pipeline.py +221 -0
  30. package/src/reidx/diagnostics/__init__.py +0 -0
  31. package/src/reidx/diagnostics/__pycache__/__init__.cpython-312.pyc +0 -0
  32. package/src/reidx/diagnostics/__pycache__/logger.cpython-312.pyc +0 -0
  33. package/src/reidx/diagnostics/logger.py +56 -0
  34. package/src/reidx/goals/__init__.py +21 -0
  35. package/src/reidx/goals/__pycache__/__init__.cpython-312.pyc +0 -0
  36. package/src/reidx/goals/__pycache__/models.cpython-312.pyc +0 -0
  37. package/src/reidx/goals/__pycache__/store.cpython-312.pyc +0 -0
  38. package/src/reidx/goals/models.py +101 -0
  39. package/src/reidx/goals/store.py +233 -0
  40. package/src/reidx/integrations/__init__.py +3 -0
  41. package/src/reidx/integrations/mcp.py +70 -0
  42. package/src/reidx/nyx/__init__.py +3 -0
  43. package/src/reidx/nyx/__pycache__/__init__.cpython-312.pyc +0 -0
  44. package/src/reidx/nyx/__pycache__/persona.cpython-312.pyc +0 -0
  45. package/src/reidx/nyx/persona.py +35 -0
  46. package/src/reidx/policy/__init__.py +15 -0
  47. package/src/reidx/policy/__pycache__/__init__.cpython-312.pyc +0 -0
  48. package/src/reidx/policy/__pycache__/engine.cpython-312.pyc +0 -0
  49. package/src/reidx/policy/__pycache__/models.cpython-312.pyc +0 -0
  50. package/src/reidx/policy/engine.py +101 -0
  51. package/src/reidx/policy/models.py +35 -0
  52. package/src/reidx/provider/__init__.py +16 -0
  53. package/src/reidx/provider/__pycache__/__init__.cpython-312.pyc +0 -0
  54. package/src/reidx/provider/__pycache__/_http.cpython-312.pyc +0 -0
  55. package/src/reidx/provider/__pycache__/anthropic.cpython-312.pyc +0 -0
  56. package/src/reidx/provider/__pycache__/base.cpython-312.pyc +0 -0
  57. package/src/reidx/provider/__pycache__/ollama.cpython-312.pyc +0 -0
  58. package/src/reidx/provider/__pycache__/openai.cpython-312.pyc +0 -0
  59. package/src/reidx/provider/__pycache__/registry.cpython-312.pyc +0 -0
  60. package/src/reidx/provider/__pycache__/store.cpython-312.pyc +0 -0
  61. package/src/reidx/provider/__pycache__/stub.cpython-312.pyc +0 -0
  62. package/src/reidx/provider/_http.py +29 -0
  63. package/src/reidx/provider/anthropic.py +176 -0
  64. package/src/reidx/provider/base.py +50 -0
  65. package/src/reidx/provider/ollama.py +109 -0
  66. package/src/reidx/provider/openai.py +144 -0
  67. package/src/reidx/provider/registry.py +88 -0
  68. package/src/reidx/provider/store.py +141 -0
  69. package/src/reidx/provider/stub.py +60 -0
  70. package/src/reidx/provider_manager/__init__.py +40 -0
  71. package/src/reidx/provider_manager/__pycache__/__init__.cpython-312.pyc +0 -0
  72. package/src/reidx/provider_manager/__pycache__/catalog.cpython-312.pyc +0 -0
  73. package/src/reidx/provider_manager/__pycache__/database.cpython-312.pyc +0 -0
  74. package/src/reidx/provider_manager/__pycache__/keychain.cpython-312.pyc +0 -0
  75. package/src/reidx/provider_manager/__pycache__/palette.cpython-312.pyc +0 -0
  76. package/src/reidx/provider_manager/catalog.py +330 -0
  77. package/src/reidx/provider_manager/database.py +234 -0
  78. package/src/reidx/provider_manager/keychain.py +102 -0
  79. package/src/reidx/provider_manager/palette.py +831 -0
  80. package/src/reidx/runtime/__init__.py +5 -0
  81. package/src/reidx/runtime/__pycache__/__init__.cpython-312.pyc +0 -0
  82. package/src/reidx/runtime/__pycache__/agent.cpython-312.pyc +0 -0
  83. package/src/reidx/runtime/__pycache__/orchestrator.cpython-312.pyc +0 -0
  84. package/src/reidx/runtime/__pycache__/reasoning.cpython-312.pyc +0 -0
  85. package/src/reidx/runtime/__pycache__/state.cpython-312.pyc +0 -0
  86. package/src/reidx/runtime/__pycache__/subagent.cpython-312.pyc +0 -0
  87. package/src/reidx/runtime/agent.py +198 -0
  88. package/src/reidx/runtime/orchestrator.py +244 -0
  89. package/src/reidx/runtime/reasoning.py +77 -0
  90. package/src/reidx/runtime/state.py +32 -0
  91. package/src/reidx/runtime/subagent.py +174 -0
  92. package/src/reidx/session/__init__.py +4 -0
  93. package/src/reidx/session/__pycache__/__init__.cpython-312.pyc +0 -0
  94. package/src/reidx/session/__pycache__/models.cpython-312.pyc +0 -0
  95. package/src/reidx/session/__pycache__/store.cpython-312.pyc +0 -0
  96. package/src/reidx/session/models.py +43 -0
  97. package/src/reidx/session/store.py +101 -0
  98. package/src/reidx/tasks/__init__.py +4 -0
  99. package/src/reidx/tasks/__pycache__/__init__.cpython-312.pyc +0 -0
  100. package/src/reidx/tasks/__pycache__/models.cpython-312.pyc +0 -0
  101. package/src/reidx/tasks/__pycache__/store.cpython-312.pyc +0 -0
  102. package/src/reidx/tasks/models.py +44 -0
  103. package/src/reidx/tasks/store.py +86 -0
  104. package/src/reidx/tools/__init__.py +44 -0
  105. package/src/reidx/tools/__pycache__/__init__.cpython-312.pyc +0 -0
  106. package/src/reidx/tools/__pycache__/base.cpython-312.pyc +0 -0
  107. package/src/reidx/tools/__pycache__/file_tools.cpython-312.pyc +0 -0
  108. package/src/reidx/tools/__pycache__/registry.cpython-312.pyc +0 -0
  109. package/src/reidx/tools/__pycache__/shell_tool.cpython-312.pyc +0 -0
  110. package/src/reidx/tools/__pycache__/spawn_agent.cpython-312.pyc +0 -0
  111. package/src/reidx/tools/__pycache__/web_tools.cpython-312.pyc +0 -0
  112. package/src/reidx/tools/base.py +84 -0
  113. package/src/reidx/tools/file_tools.py +222 -0
  114. package/src/reidx/tools/registry.py +45 -0
  115. package/src/reidx/tools/shell_tool.py +75 -0
  116. package/src/reidx/tools/spawn_agent.py +185 -0
  117. package/src/reidx/tools/web_tools.py +216 -0
  118. package/src/reidx/ui/__init__.py +41 -0
  119. package/src/reidx/ui/__pycache__/__init__.cpython-312.pyc +0 -0
  120. package/src/reidx/ui/__pycache__/app.cpython-312.pyc +0 -0
  121. package/src/reidx/ui/__pycache__/commands.cpython-312.pyc +0 -0
  122. package/src/reidx/ui/__pycache__/render.cpython-312.pyc +0 -0
  123. package/src/reidx/ui/__pycache__/repl.cpython-312.pyc +0 -0
  124. package/src/reidx/ui/__pycache__/theme.cpython-312.pyc +0 -0
  125. package/src/reidx/ui/app.py +1343 -0
  126. package/src/reidx/ui/commands.py +663 -0
  127. package/src/reidx/ui/render.py +517 -0
  128. package/src/reidx/ui/repl.py +14 -0
  129. package/src/reidx/ui/theme.py +126 -0
  130. package/src/reidx/workflows/__init__.py +4 -0
  131. package/src/reidx/workflows/__pycache__/__init__.cpython-312.pyc +0 -0
  132. package/src/reidx/workflows/__pycache__/models.cpython-312.pyc +0 -0
  133. package/src/reidx/workflows/__pycache__/store.cpython-312.pyc +0 -0
  134. package/src/reidx/workflows/models.py +19 -0
  135. 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();
@@ -0,0 +1,3 @@
1
+ """ReidX: terminal-native agent-first CLI runtime."""
2
+
3
+ __version__ = "2.0.2"
@@ -0,0 +1,9 @@
1
+ from reidx.app.commands import app
2
+
3
+
4
+ def main() -> None:
5
+ app()
6
+
7
+
8
+ if __name__ == "__main__":
9
+ main()
@@ -0,0 +1,3 @@
1
+ from reidx.app.commands import app, build_orchestrator
2
+
3
+ __all__ = ["app", "build_orchestrator"]
@@ -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__}")
@@ -0,0 +1,3 @@
1
+ from reidx.automation.exec import exec_run
2
+
3
+ __all__ = ["exec_run"]
@@ -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
@@ -0,0 +1,10 @@
1
+ from reidx.config.loader import ConfigLoader
2
+ from reidx.config.models import Config, PolicyConfig, ProviderConfig, default_config
3
+
4
+ __all__ = [
5
+ "Config",
6
+ "ConfigLoader",
7
+ "PolicyConfig",
8
+ "ProviderConfig",
9
+ "default_config",
10
+ ]
@@ -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
+ )