tamfis-code 0.2.3__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.
@@ -0,0 +1,137 @@
1
+ #!/usr/bin/env python3
2
+ """Shell completion for TAMFIS-CODE"""
3
+
4
+ import os
5
+ import sys
6
+ import argparse
7
+ from pathlib import Path
8
+ from typing import List, Dict, Optional
9
+
10
+ class ShellCompleter:
11
+ """Dynamic shell completion generator"""
12
+
13
+ SUPPORTED_SHELLS = ['bash', 'zsh', 'fish', 'powershell']
14
+
15
+ COMMANDS = {
16
+ 'chat': 'Start an interactive chat session',
17
+ 'run': 'Execute a command with context',
18
+ 'edit': 'Edit files with AI assistance',
19
+ 'review': 'Review code changes',
20
+ 'plan': 'Create an execution plan',
21
+ 'ingest': 'Ingest documents for context',
22
+ 'session': 'Manage sessions',
23
+ 'config': 'View/Edit configuration',
24
+ 'doctor': 'Run diagnostics',
25
+ 'version': 'Show version',
26
+ }
27
+
28
+ FILE_COMMANDS = ['edit', 'review', 'ingest']
29
+
30
+ @classmethod
31
+ def generate_bash(cls) -> str:
32
+ return '''_tamfis_code_completion() {
33
+ local cur prev opts files
34
+ COMPREPLY=()
35
+ cur="${COMP_WORDS[COMP_CWORD]}"
36
+ prev="${COMP_WORDS[COMP_CWORD-1]}"
37
+
38
+ opts="chat run edit review plan ingest session config doctor version"
39
+
40
+ case "${prev}" in
41
+ edit|review|ingest)
42
+ COMPREPLY=( $(compgen -f -- "${cur}") )
43
+ return 0
44
+ ;;
45
+ *)
46
+ COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
47
+ return 0
48
+ ;;
49
+ esac
50
+ }
51
+ complete -F _tamfis_code_completion tamfis-code
52
+ '''
53
+
54
+ @classmethod
55
+ def generate_zsh(cls) -> str:
56
+ return '''#compdef tamfis-code
57
+
58
+ _tamfis_code() {
59
+ local -a commands
60
+ commands=(
61
+ 'chat:Start interactive chat session'
62
+ 'run:Execute command with context'
63
+ 'edit:Edit files with AI assistance'
64
+ 'review:Review code changes'
65
+ 'plan:Create execution plan'
66
+ 'ingest:Ingest documents for context'
67
+ 'session:Manage sessions'
68
+ 'config:View/Edit configuration'
69
+ 'doctor:Run diagnostics'
70
+ 'version:Show version'
71
+ )
72
+
73
+ _arguments -C \\
74
+ '1: :->command' \\
75
+ '*:: :->args'
76
+
77
+ case $state in
78
+ command)
79
+ _describe 'commands' commands
80
+ ;;
81
+ args)
82
+ case $line[1] in
83
+ edit|review|ingest)
84
+ _files
85
+ ;;
86
+ esac
87
+ ;;
88
+ esac
89
+ }
90
+ compdef _tamfis_code tamfis-code
91
+ '''
92
+
93
+ @classmethod
94
+ def generate_fish(cls) -> str:
95
+ return '''function __fish_tamfis_code_needs_file
96
+ set cmd (commandline -opc)
97
+ contains -- $cmd[2] edit review ingest
98
+ end
99
+
100
+ complete -c tamfis-code -f
101
+ complete -c tamfis-code -n "__fish_use_subcommand" -a chat -d "Start interactive chat session"
102
+ complete -c tamfis-code -n "__fish_use_subcommand" -a run -d "Execute command with context"
103
+ complete -c tamfis-code -n "__fish_use_subcommand" -a edit -d "Edit files with AI assistance"
104
+ complete -c tamfis-code -n "__fish_use_subcommand" -a review -d "Review code changes"
105
+ complete -c tamfis-code -n "__fish_use_subcommand" -a plan -d "Create execution plan"
106
+ complete -c tamfis-code -n "__fish_use_subcommand" -a ingest -d "Ingest documents"
107
+ complete -c tamfis-code -n "__fish_use_subcommand" -a session -d "Manage sessions"
108
+ complete -c tamfis-code -n "__fish_use_subcommand" -a config -d "View/Edit configuration"
109
+ complete -c tamfis-code -n "__fish_use_subcommand" -a doctor -d "Run diagnostics"
110
+ complete -c tamfis-code -n "__fish_use_subcommand" -a version -d "Show version"
111
+
112
+ complete -c tamfis-code -n "__fish_tamfis_code_needs_file" -a "(__fish_complete_path)"
113
+ '''
114
+
115
+ @classmethod
116
+ def generate_powershell(cls) -> str:
117
+ return '''function _tamfis_code_completion {
118
+ param(
119
+ $commandName,
120
+ $parameterName,
121
+ $wordToComplete,
122
+ $commandAst,
123
+ $fakeBoundParameters
124
+ )
125
+
126
+ $commands = @(
127
+ "chat", "run", "edit", "review", "plan",
128
+ "ingest", "session", "config", "doctor", "version"
129
+ )
130
+
131
+ if ($parameterName -eq "command") {
132
+ $commands | Where-Object { $_ -like "$wordToComplete*" }
133
+ }
134
+ }
135
+
136
+ Register-ArgumentCompleter -Native -CommandName "tamfis-code" -ScriptBlock _tamfis_code_completion
137
+ '''
tamfis_code/config.py ADDED
@@ -0,0 +1,199 @@
1
+ """Configuration and credential storage for TamfisGPT Code.
2
+
3
+ Precedence (highest wins): CLI flag > environment variable > project-local
4
+ .tamfis/config.toml > user ~/.config/tamfis-code/config.toml > built-in
5
+ default. Only fields the CLI actually gives distinct behaviour to are
6
+ supported here -- see docs/REMOTE_AGENT_MASTER_SPEC.md Phase 21's full
7
+ configuration wishlist for what is intentionally deferred.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import json
13
+ import os
14
+ import stat
15
+ import tomllib
16
+ from dataclasses import dataclass, field
17
+ from pathlib import Path
18
+ from typing import Any, Optional
19
+
20
+ APPROVAL_MODES = (
21
+ "ask", "safe", "auto", "read-only", "plan-only",
22
+ "suggest", "workspace", "full-auto", "never",
23
+ "accept-edits",
24
+ )
25
+
26
+ # Short, user-facing names for /mode -- map to the policy values above.
27
+ # "ask" is this CLI's actual default/manual mode, "accept-edits" behaves
28
+ # like "safe" (auto-approve unless the server classifies the action
29
+ # dangerous, which covers ordinary file edits without covering a
30
+ # destructive shell command), "plan" is "plan-only", "auto" is itself.
31
+ MODE_ALIASES = {
32
+ "manual": "ask",
33
+ "plan": "plan-only",
34
+ "accept-edits": "accept-edits",
35
+ "auto": "auto",
36
+ }
37
+
38
+ CONFIG_DIR = Path(os.environ.get("TAMFIS_CODE_CONFIG_HOME", "") or (Path.home() / ".config" / "tamfis-code"))
39
+ CREDENTIALS_PATH = CONFIG_DIR / "credentials.json"
40
+ USER_CONFIG_PATH = CONFIG_DIR / "config.toml"
41
+ PROJECT_CONFIG_RELATIVE = Path(".tamfis") / "config.toml"
42
+
43
+ DEFAULT_API_BASE = "http://127.0.0.1:9500"
44
+
45
+
46
+ def _load_toml(path: Path) -> dict[str, Any]:
47
+ if not path.is_file():
48
+ return {}
49
+ try:
50
+ with path.open("rb") as fh:
51
+ return tomllib.load(fh)
52
+ except (OSError, tomllib.TOMLDecodeError):
53
+ return {}
54
+
55
+
56
+ @dataclass
57
+ class Config:
58
+ api_base: str = DEFAULT_API_BASE
59
+ approval_policy: str = "ask"
60
+ colour: bool = True
61
+ output_mode: str = "text"
62
+ timeout_seconds: float = 120.0
63
+ debug: bool = False
64
+ # Real (LLM-backed) subagent delegation is opt-in: concurrent sessions
65
+ # against the Remote backend have open questions (rate limiting, approval
66
+ # prompts interleaving across sessions, concurrent state.json writers)
67
+ # that need validating against a live backend before being on by default.
68
+ enable_subagent_delegation: bool = False
69
+ # "standalone" (default): call a provider directly, no TamfisGPT backend.
70
+ # "remote": use the TamfisGPT Remote Workspace backend for every command
71
+ # without needing --remote on each invocation -- set this once (via
72
+ # `tamfis-code login` writing it, or manually in config.toml) for a paid
73
+ # TamfisGPT tenant who wants that be the default, the same way Claude
74
+ # Code/Codex/kimi-code default to their respective hosted accounts.
75
+ default_backend: str = "standalone"
76
+ sources: dict[str, str] = field(default_factory=dict) # field -> where it came from, for `doctor`/`config`
77
+
78
+ def as_dict(self) -> dict[str, Any]:
79
+ return {
80
+ "api_base": self.api_base,
81
+ "approval_policy": self.approval_policy,
82
+ "colour": self.colour,
83
+ "output_mode": self.output_mode,
84
+ "timeout_seconds": self.timeout_seconds,
85
+ "debug": self.debug,
86
+ "enable_subagent_delegation": self.enable_subagent_delegation,
87
+ "default_backend": self.default_backend,
88
+ }
89
+
90
+
91
+ def load_config(project_root: Optional[Path] = None) -> Config:
92
+ cfg = Config()
93
+ cfg.sources = {k: "default" for k in cfg.as_dict()}
94
+
95
+ layers: list[tuple[str, dict[str, Any]]] = [
96
+ ("user config", _load_toml(USER_CONFIG_PATH)),
97
+ ]
98
+ if project_root is not None:
99
+ layers.append(("project config", _load_toml(project_root / PROJECT_CONFIG_RELATIVE)))
100
+
101
+ for source_name, data in layers:
102
+ if "api_base" in data:
103
+ cfg.api_base = str(data["api_base"])
104
+ cfg.sources["api_base"] = source_name
105
+ if "approval_policy" in data and data["approval_policy"] in APPROVAL_MODES:
106
+ cfg.approval_policy = str(data["approval_policy"])
107
+ cfg.sources["approval_policy"] = source_name
108
+ if "colour" in data:
109
+ cfg.colour = bool(data["colour"])
110
+ cfg.sources["colour"] = source_name
111
+ if "output_mode" in data:
112
+ cfg.output_mode = str(data["output_mode"])
113
+ cfg.sources["output_mode"] = source_name
114
+ if "timeout_seconds" in data:
115
+ cfg.timeout_seconds = float(data["timeout_seconds"])
116
+ cfg.sources["timeout_seconds"] = source_name
117
+ if "enable_subagent_delegation" in data:
118
+ cfg.enable_subagent_delegation = bool(data["enable_subagent_delegation"])
119
+ cfg.sources["enable_subagent_delegation"] = source_name
120
+ if data.get("default_backend") in ("standalone", "remote"):
121
+ cfg.default_backend = str(data["default_backend"])
122
+ cfg.sources["default_backend"] = source_name
123
+
124
+ env_api_base = os.environ.get("TAMFIS_CODE_API_BASE")
125
+ if env_api_base:
126
+ cfg.api_base = env_api_base
127
+ cfg.sources["api_base"] = "env TAMFIS_CODE_API_BASE"
128
+
129
+ env_approval = os.environ.get("TAMFIS_CODE_APPROVAL_POLICY")
130
+ if env_approval in APPROVAL_MODES:
131
+ cfg.approval_policy = env_approval
132
+ cfg.sources["approval_policy"] = "env TAMFIS_CODE_APPROVAL_POLICY"
133
+
134
+ env_delegation = os.environ.get("TAMFIS_CODE_ENABLE_SUBAGENT_DELEGATION")
135
+ if env_delegation is not None:
136
+ cfg.enable_subagent_delegation = env_delegation.lower() in {"1", "true", "yes"}
137
+ cfg.sources["enable_subagent_delegation"] = "env TAMFIS_CODE_ENABLE_SUBAGENT_DELEGATION"
138
+
139
+ env_backend = os.environ.get("TAMFIS_CODE_DEFAULT_BACKEND")
140
+ if env_backend in ("standalone", "remote"):
141
+ cfg.default_backend = env_backend
142
+ cfg.sources["default_backend"] = "env TAMFIS_CODE_DEFAULT_BACKEND"
143
+
144
+ return cfg
145
+
146
+
147
+ @dataclass
148
+ class Credentials:
149
+ access_token: str
150
+ refresh_token: Optional[str] = None
151
+ user_id: Optional[str] = None
152
+ email: Optional[str] = None
153
+
154
+
155
+ def load_credentials() -> Optional[Credentials]:
156
+ env_token = os.environ.get("TAMFIS_CODE_TOKEN")
157
+ if env_token:
158
+ return Credentials(access_token=env_token)
159
+
160
+ if not CREDENTIALS_PATH.is_file():
161
+ return None
162
+ try:
163
+ data = json.loads(CREDENTIALS_PATH.read_text())
164
+ except (OSError, json.JSONDecodeError):
165
+ return None
166
+ access_token = data.get("access_token")
167
+ if not access_token:
168
+ return None
169
+ return Credentials(
170
+ access_token=access_token,
171
+ refresh_token=data.get("refresh_token"),
172
+ user_id=data.get("user_id"),
173
+ email=data.get("email"),
174
+ )
175
+
176
+
177
+ def save_credentials(creds: Credentials) -> None:
178
+ CONFIG_DIR.mkdir(parents=True, exist_ok=True)
179
+ # Only chmod when needed -- see the identical fix in state.py's
180
+ # _save_raw for why doing this unconditionally is a landmine (raises
181
+ # PermissionError the moment CONFIG_DIR is ever owned by a different user
182
+ # than the caller).
183
+ if stat.S_IMODE(os.stat(CONFIG_DIR).st_mode) != stat.S_IRWXU:
184
+ os.chmod(CONFIG_DIR, stat.S_IRWXU) # 0700 -- owner-only, matches spec's "restrictive permissions"
185
+ payload = {
186
+ "access_token": creds.access_token,
187
+ "refresh_token": creds.refresh_token,
188
+ "user_id": creds.user_id,
189
+ "email": creds.email,
190
+ }
191
+ CREDENTIALS_PATH.write_text(json.dumps(payload))
192
+ os.chmod(CREDENTIALS_PATH, stat.S_IRUSR | stat.S_IWUSR) # 0600
193
+
194
+
195
+ def clear_credentials() -> bool:
196
+ if CREDENTIALS_PATH.is_file():
197
+ CREDENTIALS_PATH.unlink()
198
+ return True
199
+ return False
tamfis_code/doctor.py ADDED
@@ -0,0 +1,173 @@
1
+ """`tamfis-code doctor` -- connectivity/auth/workspace checks.
2
+
3
+ Reports PASS/WARNING/FAIL per Phase 21. Deliberately checks the things that
4
+ were the actual break points found during this project's Remote-workspace
5
+ audit (see docs/REMOTE_AGENT_MASTER_SPEC.md and the linked memory notes) --
6
+ API reachability, auth, and workspace-scope enforcement really working, not
7
+ just "is the process up."
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from dataclasses import dataclass
13
+ from pathlib import Path
14
+ from typing import Any, Optional
15
+
16
+ import httpx
17
+ from rich.console import Console
18
+
19
+ from .api_client import AuthRequiredError, RemoteAPIClient, RemoteAPIError
20
+ from .config import Config, load_credentials
21
+
22
+
23
+ @dataclass
24
+ class CheckResult:
25
+ name: str
26
+ status: str # "PASS" | "WARNING" | "FAIL"
27
+ detail: str = ""
28
+
29
+
30
+ _STATUS_STYLE = {"PASS": "green", "WARNING": "yellow", "FAIL": "red"}
31
+
32
+ _REUSABLE_SESSION_STATUSES = {"idle", "active"}
33
+
34
+
35
+ def check_event_sequence_integrity(events: list[dict[str, Any]]) -> CheckResult:
36
+ """Pure check over a window of a session's RemoteEvent replay (as
37
+ returned by GET .../thread): sequence numbers must be unique and, within
38
+ the returned window, contiguous. A duplicate is a real replay-safety
39
+ bug (the same event could be rendered/acted on twice by a client that
40
+ resumes from `last_event_id`); a gap is either dropped events or just
41
+ this window being truncated by the endpoint's own `limit` -- reported
42
+ as a WARNING, not a FAIL, since it can't be told apart from here.
43
+ """
44
+ if not events:
45
+ return CheckResult("Event replay integrity", "WARNING", "no events yet for this session")
46
+ sequences = [e.get("sequence") for e in events if isinstance(e.get("sequence"), int)]
47
+ if len(sequences) != len(events):
48
+ return CheckResult("Event replay integrity", "FAIL", "one or more events are missing a sequence number")
49
+ sequences.sort()
50
+ duplicates = {s for s in sequences if sequences.count(s) > 1}
51
+ if duplicates:
52
+ return CheckResult("Event replay integrity", "FAIL", f"duplicate sequence number(s): {sorted(duplicates)[:5]}")
53
+ gaps = [b for a, b in zip(sequences, sequences[1:]) if b != a + 1]
54
+ if gaps:
55
+ return CheckResult(
56
+ "Event replay integrity", "WARNING",
57
+ f"{len(gaps)} gap(s) in {len(sequences)} events checked (sequence {sequences[0]}-{sequences[-1]}) "
58
+ "-- may just be this check's window, or a dropped event",
59
+ )
60
+ return CheckResult("Event replay integrity", "PASS", f"{len(sequences)} events, sequence {sequences[0]}-{sequences[-1]}, no gaps or duplicates")
61
+
62
+
63
+ async def _diagnose_session(client: RemoteAPIClient, session_id: int, workspace_root: Optional[Path]) -> list[CheckResult]:
64
+ """Session/workspace-snapshot/event-replay health for the *active*
65
+ session -- distinct from the generic connectivity checks above, this is
66
+ the "am I actually in the state I think I'm in" self-diagnosis."""
67
+ results: list[CheckResult] = []
68
+ try:
69
+ session = await client.get_session(session_id)
70
+ except (AuthRequiredError, RemoteAPIError) as e:
71
+ results.append(CheckResult("Active session", "FAIL", f"session {session_id}: {e}"))
72
+ return results
73
+
74
+ status = str(session.get("status") or "")
75
+ if status in _REUSABLE_SESSION_STATUSES:
76
+ results.append(CheckResult("Active session", "PASS", f"session {session_id} status={status}"))
77
+ else:
78
+ results.append(CheckResult("Active session", "WARNING", f"session {session_id} status={status} (not idle/active)"))
79
+
80
+ server_wd = session.get("working_directory")
81
+ if workspace_root is not None and server_wd:
82
+ if str(workspace_root.resolve()) == server_wd:
83
+ results.append(CheckResult("Session cwd matches local cwd", "PASS", server_wd))
84
+ else:
85
+ results.append(CheckResult(
86
+ "Session cwd matches local cwd", "WARNING",
87
+ f"server has {server_wd!r}, local cwd is {str(workspace_root.resolve())!r}",
88
+ ))
89
+
90
+ snapshot = session.get("workspace_snapshot")
91
+ if snapshot is None:
92
+ results.append(CheckResult("Workspace snapshot", "WARNING", "not scanned yet -- will populate on the first AI task"))
93
+ else:
94
+ detail = (
95
+ f"v{snapshot.get('file_index_version')}, "
96
+ f"repo={snapshot.get('repository_type')}, "
97
+ f"branch={snapshot.get('git_branch') or '-'}, "
98
+ f"last scan={snapshot.get('last_scan_at') or 'unknown'} "
99
+ f"(reason: {snapshot.get('scan_reason') or 'unknown'})"
100
+ )
101
+ results.append(CheckResult("Workspace snapshot", "PASS", detail))
102
+
103
+ try:
104
+ thread = await client.get_thread(session_id, after_sequence=0)
105
+ results.append(check_event_sequence_integrity(thread.get("events") or []))
106
+ except (AuthRequiredError, RemoteAPIError) as e:
107
+ results.append(CheckResult("Event replay integrity", "FAIL", str(e)))
108
+
109
+ return results
110
+
111
+
112
+ async def run_doctor(
113
+ config: Config,
114
+ console: Console,
115
+ workspace_root: Optional[Path] = None,
116
+ *,
117
+ session_id: Optional[int] = None,
118
+ ) -> bool:
119
+ results: list[CheckResult] = []
120
+
121
+ results.append(CheckResult("Config", "PASS", f"api_base={config.api_base}"))
122
+
123
+ creds = load_credentials()
124
+ if creds is None:
125
+ results.append(CheckResult("Authentication", "FAIL", "No credentials -- run `tamfis-code login`"))
126
+ else:
127
+ results.append(CheckResult("Authentication", "PASS", f"credentials present for {creds.email or creds.user_id or 'unknown user'}"))
128
+
129
+ client = RemoteAPIClient(config, creds)
130
+ try:
131
+ try:
132
+ servers = await client.list_servers()
133
+ results.append(CheckResult("Remote API (Tier III, port 9500)", "PASS", f"{len(servers)} registered server(s)"))
134
+ except AuthRequiredError as e:
135
+ results.append(CheckResult("Remote API (Tier III, port 9500)", "FAIL", str(e)))
136
+ servers = None
137
+ except (RemoteAPIError, httpx.HTTPError) as e:
138
+ results.append(CheckResult("Remote API (Tier III, port 9500)", "FAIL", str(e)))
139
+ servers = None
140
+
141
+ if servers is not None:
142
+ local_server = next((s for s in servers if s.get("transport_type") == "local"), None)
143
+ if local_server is not None:
144
+ results.append(CheckResult("Local transport server", "PASS", f"server_id={local_server['id']}"))
145
+ else:
146
+ results.append(CheckResult("Local transport server", "WARNING", "none registered yet -- `tamfis-code init` will create one"))
147
+
148
+ if workspace_root is not None:
149
+ wr = str(workspace_root.resolve())
150
+ if workspace_root.is_dir():
151
+ results.append(CheckResult("Workspace directory", "PASS", wr))
152
+ else:
153
+ results.append(CheckResult("Workspace directory", "FAIL", f"{wr} is not a directory"))
154
+
155
+ # Tier IV reachability is inferred, not probed directly -- there is
156
+ # no public health endpoint on port 9555 to hit from here without
157
+ # a session already existing; a real ai-task submission is what
158
+ # actually proves the whole chain, which `doctor` deliberately does
159
+ # not do (it would create session/task rows as a side effect of a
160
+ # health check).
161
+ results.append(CheckResult("Tier IV (agent runtime)", "WARNING", "not directly probed -- verified indirectly via a real `tamfis-code ask`"))
162
+
163
+ if session_id is not None:
164
+ results.extend(await _diagnose_session(client, session_id, workspace_root))
165
+
166
+ finally:
167
+ await client.aclose()
168
+
169
+ for result in results:
170
+ style = _STATUS_STYLE[result.status]
171
+ console.print(f"[{style}]{result.status:8}[/{style}] {result.name} [dim]{result.detail}[/dim]")
172
+
173
+ return all(r.status != "FAIL" for r in results)