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 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
@@ -0,0 +1,3 @@
1
+ # Overwritten by scripts/install.sh when packaging; leave empty for source checkouts
2
+ # (runtime falls back to git rev-parse).
3
+ COMMIT = ""
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)