axism 0.1.1__py3-none-any.whl
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.
- axism/__init__.py +50 -0
- axism/_build_info.py +3 -0
- axism/agents.py +151 -0
- axism/cli.py +726 -0
- axism/delete.py +509 -0
- axism/discover.py +247 -0
- axism/fragments.py +229 -0
- axism/live.py +503 -0
- axism/move.py +751 -0
- axism/paths.py +102 -0
- axism/providers/__init__.py +99 -0
- axism/providers/base.py +296 -0
- axism/providers/claude_code.py +209 -0
- axism/providers/common.py +184 -0
- axism/providers/cursor.py +291 -0
- axism/providers/cursor_store/__init__.py +1 -0
- axism/providers/cursor_store/discover.py +326 -0
- axism/providers/cursor_store/fragments.py +42 -0
- axism/providers/cursor_store/live.py +26 -0
- axism/providers/cursor_store/ops.py +207 -0
- axism/providers/cursor_store/paths.py +139 -0
- axism/providers/federation.py +101 -0
- axism/providers/hermes.py +293 -0
- axism/providers/hermes_store/__init__.py +1 -0
- axism/providers/hermes_store/discover.py +175 -0
- axism/providers/hermes_store/fragments.py +59 -0
- axism/providers/hermes_store/ops.py +65 -0
- axism/providers/hermes_store/paths.py +57 -0
- axism/providers/transfer.py +639 -0
- axism/rename.py +64 -0
- axism/settings.py +125 -0
- axism/tui/__init__.py +1 -0
- axism/tui/app.py +2768 -0
- axism-0.1.1.dist-info/METADATA +275 -0
- axism-0.1.1.dist-info/RECORD +38 -0
- axism-0.1.1.dist-info/WHEEL +4 -0
- axism-0.1.1.dist-info/entry_points.txt +2 -0
- axism-0.1.1.dist-info/licenses/LICENSE +21 -0
axism/__init__.py
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""aXism: Agent Interactive Session Manager."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import subprocess
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
__version__ = "0.1.1"
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _commit_from_build_info() -> str:
|
|
12
|
+
try:
|
|
13
|
+
from axism._build_info import COMMIT
|
|
14
|
+
except ImportError:
|
|
15
|
+
return ""
|
|
16
|
+
return (COMMIT or "").strip()
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _commit_from_git() -> str:
|
|
20
|
+
"""Short hash when running from a checkout (or any ancestor with .git)."""
|
|
21
|
+
here = Path(__file__).resolve().parent
|
|
22
|
+
for root in (here, *here.parents):
|
|
23
|
+
if not (root / ".git").exists():
|
|
24
|
+
continue
|
|
25
|
+
try:
|
|
26
|
+
out = subprocess.run(
|
|
27
|
+
["git", "-C", str(root), "rev-parse", "--short=7", "HEAD"],
|
|
28
|
+
check=True,
|
|
29
|
+
capture_output=True,
|
|
30
|
+
text=True,
|
|
31
|
+
timeout=2,
|
|
32
|
+
)
|
|
33
|
+
except (OSError, subprocess.SubprocessError):
|
|
34
|
+
return ""
|
|
35
|
+
return out.stdout.strip()
|
|
36
|
+
return ""
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _resolve_commit() -> str:
|
|
40
|
+
return _commit_from_build_info() or _commit_from_git()
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
__commit__ = _resolve_commit()
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def version_string() -> str:
|
|
47
|
+
"""Human-facing version, with short commit when known (e.g. ``0.1.0+a1b2c3d``)."""
|
|
48
|
+
if __commit__:
|
|
49
|
+
return f"{__version__}+{__commit__}"
|
|
50
|
+
return __version__
|
axism/_build_info.py
ADDED
axism/agents.py
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
"""Discover local coding-agent CLIs and prepare memory-merge handoff briefs."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import shutil
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from axism.move import MemoryStats, MoveResult
|
|
11
|
+
from axism.paths import config_root
|
|
12
|
+
|
|
13
|
+
# Preference order for --memory-agent auto
|
|
14
|
+
AGENT_PREFERENCE = ("claude", "cursor", "opencode", "aider", "codex")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass(frozen=True)
|
|
18
|
+
class AgentCLI:
|
|
19
|
+
id: str
|
|
20
|
+
label: str
|
|
21
|
+
binary: str
|
|
22
|
+
|
|
23
|
+
def handoff_argv(self, brief_path: Path) -> list[str]:
|
|
24
|
+
"""Build argv to start this agent with the merge brief (no shell)."""
|
|
25
|
+
brief = str(brief_path)
|
|
26
|
+
prompt = (
|
|
27
|
+
f"Read and follow the task brief at {brief}. "
|
|
28
|
+
"Stay within the boundaries listed there."
|
|
29
|
+
)
|
|
30
|
+
if self.id == "claude":
|
|
31
|
+
return [self.binary, "-p", prompt]
|
|
32
|
+
if self.id == "aider":
|
|
33
|
+
return [self.binary, "--message", prompt]
|
|
34
|
+
if self.id == "opencode":
|
|
35
|
+
return [self.binary, "run", prompt]
|
|
36
|
+
if self.id == "codex":
|
|
37
|
+
return [self.binary, "exec", prompt]
|
|
38
|
+
if self.id == "cursor":
|
|
39
|
+
# Best-effort; many installs only expose the GUI binary.
|
|
40
|
+
return [self.binary, "agent", prompt]
|
|
41
|
+
return [self.binary, prompt]
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _which_first(*names: str) -> str | None:
|
|
45
|
+
for name in names:
|
|
46
|
+
found = shutil.which(name)
|
|
47
|
+
if found:
|
|
48
|
+
return found
|
|
49
|
+
return None
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def detect_agents() -> list[AgentCLI]:
|
|
53
|
+
"""Return agent CLIs available on PATH, in preference order."""
|
|
54
|
+
specs: list[tuple[str, str, tuple[str, ...]]] = [
|
|
55
|
+
("claude", "Claude Code", ("claude",)),
|
|
56
|
+
("cursor", "Cursor agent", ("cursor-agent", "cursor")),
|
|
57
|
+
("opencode", "OpenCode", ("opencode",)),
|
|
58
|
+
("aider", "Aider", ("aider",)),
|
|
59
|
+
("codex", "Codex", ("codex",)),
|
|
60
|
+
]
|
|
61
|
+
out: list[AgentCLI] = []
|
|
62
|
+
for aid, label, bins in specs:
|
|
63
|
+
binary = _which_first(*bins)
|
|
64
|
+
if binary:
|
|
65
|
+
out.append(AgentCLI(id=aid, label=label, binary=binary))
|
|
66
|
+
return out
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def pick_auto_agent(agents: list[AgentCLI] | None = None) -> AgentCLI | None:
|
|
70
|
+
agents = agents if agents is not None else detect_agents()
|
|
71
|
+
by_id = {a.id: a for a in agents}
|
|
72
|
+
for aid in AGENT_PREFERENCE:
|
|
73
|
+
if aid in by_id:
|
|
74
|
+
return by_id[aid]
|
|
75
|
+
return agents[0] if agents else None
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def brief_path(root: Path | None = None) -> Path:
|
|
79
|
+
root = root or config_root()
|
|
80
|
+
return root / ".axism-memory-merge-task.md"
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def write_memory_merge_brief(
|
|
84
|
+
result: MoveResult,
|
|
85
|
+
*,
|
|
86
|
+
root: Path | None = None,
|
|
87
|
+
force: bool = False,
|
|
88
|
+
) -> Path:
|
|
89
|
+
"""Write an agent-agnostic task brief for finishing MEMORY.md merge."""
|
|
90
|
+
root = root or config_root()
|
|
91
|
+
path = brief_path(root)
|
|
92
|
+
dest = result.memory_dest
|
|
93
|
+
stats = result.memory_stats or MemoryStats()
|
|
94
|
+
parked = "\n".join(f"- `{p}`" for p in result.memory_parked) or "- (none)"
|
|
95
|
+
renamed = "\n".join(f"- `{p}`" for p in result.memory_renamed) or "- (none)"
|
|
96
|
+
force_note = (
|
|
97
|
+
"\n**User overrode size/complexity limits.** Still refuse if you cannot "
|
|
98
|
+
"complete a safe merge without inventing content or touching other paths.\n"
|
|
99
|
+
if force
|
|
100
|
+
else ""
|
|
101
|
+
)
|
|
102
|
+
body = f"""# aXism memory merge task
|
|
103
|
+
|
|
104
|
+
Merge project memory indexes after an aXism project move.
|
|
105
|
+
{force_note}
|
|
106
|
+
## Paths
|
|
107
|
+
|
|
108
|
+
- Destination memory dir: `{dest}`
|
|
109
|
+
- Parked source MEMORY files:
|
|
110
|
+
{parked}
|
|
111
|
+
- Renamed collision topic files:
|
|
112
|
+
{renamed}
|
|
113
|
+
|
|
114
|
+
Stats: {stats.summary()}
|
|
115
|
+
|
|
116
|
+
## Goal
|
|
117
|
+
|
|
118
|
+
1. Produce one coherent `MEMORY.md` in the destination memory dir.
|
|
119
|
+
2. Keep all topic `.md` files; prefer destination wording when deduping.
|
|
120
|
+
3. Ensure every topic file is linked from `MEMORY.md`.
|
|
121
|
+
4. Do not invent facts; only reorganize and reconcile existing markdown.
|
|
122
|
+
|
|
123
|
+
## Hard boundaries
|
|
124
|
+
|
|
125
|
+
- Only read/write files under the destination memory directory listed above
|
|
126
|
+
(including parked `MEMORY-from-*.md` files already moved there).
|
|
127
|
+
- Do **not** edit session `.jsonl` transcripts, jobs, credentials, settings,
|
|
128
|
+
skills, plugins, or anything outside that memory directory.
|
|
129
|
+
- Do **not** use the network or make git commits.
|
|
130
|
+
- Do **not** delete the destination `MEMORY.md` without replacing it.
|
|
131
|
+
|
|
132
|
+
## Success criteria
|
|
133
|
+
|
|
134
|
+
- Single canonical `MEMORY.md` in the destination memory dir.
|
|
135
|
+
- No orphaned topic files without an index link.
|
|
136
|
+
- Parked `MEMORY-from-*.md` files may be removed only after their content is
|
|
137
|
+
merged into the canonical index (or clearly noted as obsolete).
|
|
138
|
+
|
|
139
|
+
## If too hard
|
|
140
|
+
|
|
141
|
+
Stop. Write `MERGE_ABORTED.md` in the destination memory dir explaining why,
|
|
142
|
+
and leave other files unchanged as much as possible.
|
|
143
|
+
"""
|
|
144
|
+
path.write_text(body, encoding="utf-8")
|
|
145
|
+
return path
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def exec_agent_handoff(agent: AgentCLI, brief: Path) -> None:
|
|
149
|
+
"""Replace this process with the chosen agent CLI. Never returns on success."""
|
|
150
|
+
argv = agent.handoff_argv(brief)
|
|
151
|
+
os.execvp(argv[0], argv)
|