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.
- tamfis_code/__init__.py +58 -0
- tamfis_code/__main__.py +4 -0
- tamfis_code/agents.py +371 -0
- tamfis_code/api_client.py +461 -0
- tamfis_code/cli.py +2351 -0
- tamfis_code/completion.py +137 -0
- tamfis_code/config.py +199 -0
- tamfis_code/doctor.py +173 -0
- tamfis_code/enforcer.py +318 -0
- tamfis_code/indexer.py +567 -0
- tamfis_code/instructions.py +121 -0
- tamfis_code/interactive.py +985 -0
- tamfis_code/local_chat.py +117 -0
- tamfis_code/local_tools.py +93 -0
- tamfis_code/mcp.py +538 -0
- tamfis_code/metrics.py +105 -0
- tamfis_code/providers.py +423 -0
- tamfis_code/references.py +336 -0
- tamfis_code/render.py +575 -0
- tamfis_code/runner.py +635 -0
- tamfis_code/runner_local.py +364 -0
- tamfis_code/safety.py +164 -0
- tamfis_code/screenshot.py +382 -0
- tamfis_code/sessions.py +253 -0
- tamfis_code/state.py +449 -0
- tamfis_code/tasks.py +42 -0
- tamfis_code/workspace.py +329 -0
- tamfis_code-0.2.3.dist-info/METADATA +78 -0
- tamfis_code-0.2.3.dist-info/RECORD +32 -0
- tamfis_code-0.2.3.dist-info/WHEEL +5 -0
- tamfis_code-0.2.3.dist-info/entry_points.txt +4 -0
- tamfis_code-0.2.3.dist-info/top_level.txt +1 -0
tamfis_code/workspace.py
ADDED
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
"""Resolves the launch directory into a Remote session, idempotently.
|
|
2
|
+
|
|
3
|
+
Implements Phase 21's "CLI entry behaviour": running `tamfis-code` from a
|
|
4
|
+
directory treats that directory as workspace_root, never scans the wider
|
|
5
|
+
box, and reuses (rather than re-creates) the local server/session pair for
|
|
6
|
+
repeated runs from the same directory.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import hashlib
|
|
12
|
+
import re
|
|
13
|
+
import subprocess
|
|
14
|
+
from dataclasses import dataclass
|
|
15
|
+
from datetime import datetime, timezone
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from typing import Any, Optional
|
|
18
|
+
|
|
19
|
+
from .api_client import RemoteAPIClient
|
|
20
|
+
from . import state as local_state
|
|
21
|
+
|
|
22
|
+
REUSABLE_STATUSES = {"idle", "active"}
|
|
23
|
+
INSTRUCTION_NAMES = {"AGENTS.md", "CLAUDE.md", "CODEX.md", "CONTRIBUTING.md", "README.md"}
|
|
24
|
+
REPORT_RE = re.compile(
|
|
25
|
+
r"(report|audit|analysis|architecture|implementation|codex|claude|tamfis[-_ ]?code|"
|
|
26
|
+
r"terminal|agent|roadmap|plan|findings|gaps|status|handover|review)", re.I,
|
|
27
|
+
)
|
|
28
|
+
REPORT_SUFFIXES = {".md", ".txt", ".json", ".yaml", ".yml", ".toml", ".log", ".pdf", ".docx"}
|
|
29
|
+
IGNORED_PARTS = {".git", "node_modules", ".venv", "venv", "dist", "build", "__pycache__"}
|
|
30
|
+
DISPOSABLE_UNTRACKED_PARTS = {
|
|
31
|
+
"__pycache__", ".pytest_cache", ".mypy_cache", ".ruff_cache",
|
|
32
|
+
".tox", ".nox", ".coverage", "htmlcov", ".DS_Store",
|
|
33
|
+
}
|
|
34
|
+
MAX_INDEX_FILES = 20_000
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass
|
|
38
|
+
class WorkspaceContext:
|
|
39
|
+
session_id: int
|
|
40
|
+
workspace_root: str
|
|
41
|
+
# None for a purely local session (see resolve_local_workspace) -- only
|
|
42
|
+
# meaningful for the legacy Remote-backend path (resolve_workspace),
|
|
43
|
+
# which this field exists to support until that path is retired.
|
|
44
|
+
server_id: Optional[int] = None
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _next_local_session_id() -> int:
|
|
48
|
+
known = local_state.all_known_session_ids()
|
|
49
|
+
return (max(known) + 1) if known else 1
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def resolve_local_workspace(cwd: Optional[Path] = None, *, discover: bool = True) -> WorkspaceContext:
|
|
53
|
+
"""Resolve a launch directory into a purely local session -- no network
|
|
54
|
+
calls, no RemoteAPIClient, no remote-assigned session/server id.
|
|
55
|
+
|
|
56
|
+
Reuses a prior session for the same workspace_root (matched by
|
|
57
|
+
`primary_workspace`, the same durable-root concept `resolve_workspace`
|
|
58
|
+
already used) rather than minting a new one on every invocation; a
|
|
59
|
+
fresh id is allocated via `_next_local_session_id()` (one past the
|
|
60
|
+
highest session id state.py already knows about) when no match exists.
|
|
61
|
+
"""
|
|
62
|
+
workspace_root = str((cwd or Path.cwd()).resolve())
|
|
63
|
+
|
|
64
|
+
local_match = next((
|
|
65
|
+
sid for sid in reversed(local_state.all_known_session_ids())
|
|
66
|
+
if local_state.get_session_state(sid).primary_workspace == workspace_root
|
|
67
|
+
), None)
|
|
68
|
+
session_id = local_match if local_match is not None else _next_local_session_id()
|
|
69
|
+
|
|
70
|
+
local_state.save_session_state(session_id, workspace_root=workspace_root)
|
|
71
|
+
if discover:
|
|
72
|
+
discover_local_repository(session_id, Path(workspace_root))
|
|
73
|
+
return WorkspaceContext(session_id=session_id, workspace_root=workspace_root)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
async def _get_or_create_local_server(client: RemoteAPIClient) -> dict:
|
|
77
|
+
servers = await client.list_servers()
|
|
78
|
+
for server in servers:
|
|
79
|
+
if server.get("transport_type") == "local":
|
|
80
|
+
return server
|
|
81
|
+
created = await client.register_local_server("local-vps")
|
|
82
|
+
return created["server"]
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
async def resolve_workspace(
|
|
86
|
+
client: RemoteAPIClient, cwd: Optional[Path] = None, *, discover: bool = True,
|
|
87
|
+
) -> WorkspaceContext:
|
|
88
|
+
workspace_root = str((cwd or Path.cwd()).resolve())
|
|
89
|
+
|
|
90
|
+
server = await _get_or_create_local_server(client)
|
|
91
|
+
server_id = server["id"]
|
|
92
|
+
|
|
93
|
+
# A session can move to an explicitly approved sibling workspace. Reuse
|
|
94
|
+
# it by its durable primary root rather than creating a new conversation
|
|
95
|
+
# just because its current working directory changed.
|
|
96
|
+
local_match = next((
|
|
97
|
+
sid for sid in reversed(local_state.all_known_session_ids())
|
|
98
|
+
if local_state.get_session_state(sid).primary_workspace == workspace_root
|
|
99
|
+
), None)
|
|
100
|
+
if local_match is not None:
|
|
101
|
+
try:
|
|
102
|
+
detail = await client.get_session(local_match)
|
|
103
|
+
except Exception:
|
|
104
|
+
detail = None
|
|
105
|
+
if detail and str(detail.get("status", "")).lower() in REUSABLE_STATUSES:
|
|
106
|
+
state = local_state.get_session_state(local_match)
|
|
107
|
+
current = str(detail.get("working_directory") or state.current_working_directory or workspace_root)
|
|
108
|
+
local_state.save_session_state(
|
|
109
|
+
local_match, workspace_root=workspace_root,
|
|
110
|
+
current_working_directory=current,
|
|
111
|
+
)
|
|
112
|
+
if discover:
|
|
113
|
+
discover_local_repository(local_match, Path(current))
|
|
114
|
+
return WorkspaceContext(session_id=local_match, server_id=server_id, workspace_root=current)
|
|
115
|
+
|
|
116
|
+
sessions = await client.list_sessions()
|
|
117
|
+
existing = next(
|
|
118
|
+
(
|
|
119
|
+
s for s in sessions
|
|
120
|
+
if s.get("server_id") == server_id
|
|
121
|
+
and s.get("working_directory") == workspace_root
|
|
122
|
+
and str(s.get("status", "")).lower() in REUSABLE_STATUSES
|
|
123
|
+
),
|
|
124
|
+
None,
|
|
125
|
+
)
|
|
126
|
+
if existing is not None:
|
|
127
|
+
local_state.save_session_state(existing["id"], workspace_root=workspace_root)
|
|
128
|
+
if discover:
|
|
129
|
+
discover_local_repository(existing["id"], Path(workspace_root))
|
|
130
|
+
return WorkspaceContext(session_id=existing["id"], server_id=server_id, workspace_root=workspace_root)
|
|
131
|
+
|
|
132
|
+
created = await client.create_session(server_id, workspace_root)
|
|
133
|
+
local_state.save_session_state(created["id"], workspace_root=workspace_root)
|
|
134
|
+
if discover:
|
|
135
|
+
discover_local_repository(created["id"], Path(workspace_root))
|
|
136
|
+
return WorkspaceContext(session_id=created["id"], server_id=server_id, workspace_root=workspace_root)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
async def context_from_session(client: RemoteAPIClient, session_id: int) -> WorkspaceContext:
|
|
140
|
+
"""Builds a WorkspaceContext from an existing session_id -- used by
|
|
141
|
+
`/resume` and `tamfis-code resume`. Raises RemoteAPIError (404) if the
|
|
142
|
+
session does not exist or is not owned by the authenticated user, same
|
|
143
|
+
ownership check every other Remote endpoint already enforces."""
|
|
144
|
+
detail = await client.get_session(session_id)
|
|
145
|
+
workspace_root = detail.get("working_directory") or ""
|
|
146
|
+
local_state.save_session_state(session_id, workspace_root=workspace_root)
|
|
147
|
+
if workspace_root:
|
|
148
|
+
discover_local_repository(session_id, Path(workspace_root))
|
|
149
|
+
return WorkspaceContext(
|
|
150
|
+
session_id=detail["id"],
|
|
151
|
+
server_id=detail["server_id"],
|
|
152
|
+
workspace_root=workspace_root,
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
async def find_resumable_session(client: RemoteAPIClient, *, exclude_session_id: Optional[int] = None) -> Optional[dict]:
|
|
157
|
+
"""Most recent non-closed session other than the one already active --
|
|
158
|
+
`list_sessions()` is ordered by created_at desc server-side."""
|
|
159
|
+
sessions = await client.list_sessions()
|
|
160
|
+
for session in sessions:
|
|
161
|
+
if exclude_session_id is not None and session.get("id") == exclude_session_id:
|
|
162
|
+
continue
|
|
163
|
+
if str(session.get("status", "")).lower() in REUSABLE_STATUSES:
|
|
164
|
+
return session
|
|
165
|
+
return None
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _git(root: Path, *args: str) -> str:
|
|
169
|
+
"""Run a bounded, argument-safe Git query (never through a shell)."""
|
|
170
|
+
try:
|
|
171
|
+
result = subprocess.run(
|
|
172
|
+
["git", "-C", str(root), *args], capture_output=True, text=True,
|
|
173
|
+
timeout=5, check=False,
|
|
174
|
+
)
|
|
175
|
+
except (OSError, subprocess.TimeoutExpired):
|
|
176
|
+
return ""
|
|
177
|
+
return result.stdout.strip() if result.returncode == 0 else ""
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def _indexable_files(root: Path) -> list[Path]:
|
|
181
|
+
output = _git(root, "ls-files", "-co", "--exclude-standard")
|
|
182
|
+
if output:
|
|
183
|
+
return [root / item for item in output.splitlines()[:MAX_INDEX_FILES]]
|
|
184
|
+
found: list[Path] = []
|
|
185
|
+
for path in root.rglob("*"):
|
|
186
|
+
if any(part in IGNORED_PARTS for part in path.parts):
|
|
187
|
+
continue
|
|
188
|
+
if path.is_file():
|
|
189
|
+
found.append(path)
|
|
190
|
+
if len(found) >= MAX_INDEX_FILES:
|
|
191
|
+
break
|
|
192
|
+
return found
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def blocking_dirty_files(status_lines: list[str]) -> list[str]:
|
|
196
|
+
"""Return entries that may represent user-authored work.
|
|
197
|
+
|
|
198
|
+
Tracked changes always block execute mode. Only untracked, well-known
|
|
199
|
+
disposable test/interpreter artefacts are ignored.
|
|
200
|
+
"""
|
|
201
|
+
blocking: list[str] = []
|
|
202
|
+
for raw in status_lines:
|
|
203
|
+
line = str(raw).rstrip()
|
|
204
|
+
if not line:
|
|
205
|
+
continue
|
|
206
|
+
status = line[:2]
|
|
207
|
+
path_text = line[3:].strip() if len(line) > 3 else ""
|
|
208
|
+
parts = {part for part in Path(path_text).parts if part not in {".", ""}}
|
|
209
|
+
if status == "??" and parts.intersection(DISPOSABLE_UNTRACKED_PARTS):
|
|
210
|
+
continue
|
|
211
|
+
blocking.append(line)
|
|
212
|
+
return blocking
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def _report_title(path: Path) -> str:
|
|
216
|
+
if path.suffix.lower() in {".md", ".txt"}:
|
|
217
|
+
try:
|
|
218
|
+
for line in path.read_text(encoding="utf-8", errors="replace").splitlines()[:40]:
|
|
219
|
+
if line.lstrip().startswith("#"):
|
|
220
|
+
return line.lstrip("# ").strip()
|
|
221
|
+
except OSError:
|
|
222
|
+
pass
|
|
223
|
+
return path.stem.replace("_", " ").replace("-", " ")
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def discover_local_repository(session_id: int, workspace_root: Path, *, force: bool = False) -> dict[str, Any]:
|
|
227
|
+
"""Cache local CLI context until Git HEAD or dirty-file count changes.
|
|
228
|
+
|
|
229
|
+
The model-facing server snapshot remains authoritative. This lightweight
|
|
230
|
+
index powers offline `/context` and `/reports`, and records the worktree
|
|
231
|
+
state that existed before a task begins.
|
|
232
|
+
"""
|
|
233
|
+
root_text = _git(workspace_root, "rev-parse", "--show-toplevel")
|
|
234
|
+
root = Path(root_text).resolve() if root_text else workspace_root.resolve()
|
|
235
|
+
branch = _git(root, "branch", "--show-current") or None
|
|
236
|
+
head = _git(root, "rev-parse", "HEAD") or "no-head"
|
|
237
|
+
dirty_lines = _git(root, "status", "--short").splitlines()
|
|
238
|
+
fingerprint = hashlib.sha256(
|
|
239
|
+
f"{root}|{head}|{'|'.join(dirty_lines)}".encode()
|
|
240
|
+
).hexdigest()
|
|
241
|
+
current = local_state.get_session_state(session_id)
|
|
242
|
+
if not force and current.discovery_fingerprint == fingerprint and current.repository_context:
|
|
243
|
+
return current.repository_context
|
|
244
|
+
|
|
245
|
+
files = _indexable_files(root)
|
|
246
|
+
instructions: list[str] = []
|
|
247
|
+
reports: list[dict[str, Any]] = []
|
|
248
|
+
for path in files:
|
|
249
|
+
if path.name in INSTRUCTION_NAMES:
|
|
250
|
+
instructions.append(str(path))
|
|
251
|
+
if path.suffix.lower() in REPORT_SUFFIXES and REPORT_RE.search(path.name):
|
|
252
|
+
try:
|
|
253
|
+
modified = datetime.fromtimestamp(path.stat().st_mtime, timezone.utc).isoformat()
|
|
254
|
+
except OSError:
|
|
255
|
+
modified = ""
|
|
256
|
+
reports.append({
|
|
257
|
+
"path": str(path), "title": _report_title(path), "modified_at": modified,
|
|
258
|
+
"scope": "repository", "verification": "unverified",
|
|
259
|
+
})
|
|
260
|
+
|
|
261
|
+
context = {
|
|
262
|
+
"repository_root": str(root), "working_directory": str(workspace_root.resolve()),
|
|
263
|
+
"branch": branch, "head": None if head == "no-head" else head,
|
|
264
|
+
"dirty": bool(dirty_lines), "dirty_files": dirty_lines[:100],
|
|
265
|
+
"blocking_dirty_files": blocking_dirty_files(dirty_lines)[:100],
|
|
266
|
+
"instruction_files": sorted(instructions), "indexed_file_count": len(files),
|
|
267
|
+
"indexed_at": datetime.now(timezone.utc).isoformat(),
|
|
268
|
+
}
|
|
269
|
+
local_state.save_session_state(
|
|
270
|
+
session_id, repository_root=str(root), current_working_directory=str(workspace_root.resolve()),
|
|
271
|
+
active_branch=branch, repository_context=context,
|
|
272
|
+
discovered_reports=sorted(reports, key=lambda item: item["modified_at"], reverse=True),
|
|
273
|
+
discovery_fingerprint=fingerprint,
|
|
274
|
+
)
|
|
275
|
+
return context
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
# Total instruction-file content budget for the system prompt -- generous
|
|
279
|
+
# enough for a real AGENTS.md/CLAUDE.md, bounded so a huge README doesn't
|
|
280
|
+
# blow the context window before the actual objective is even sent.
|
|
281
|
+
MAX_INSTRUCTION_CHARS = 12_000
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def build_system_prompt(session_id: int, workspace_root: Path, *, force_discovery: bool = False) -> str:
|
|
285
|
+
"""Build the standalone agent loop's system prompt from real local
|
|
286
|
+
workspace awareness (git branch/dirty status, instruction file
|
|
287
|
+
contents) -- replacing the context tamgpt6 used to assemble server-side.
|
|
288
|
+
Reuses discover_local_repository's existing fingerprint-cached scan
|
|
289
|
+
rather than re-walking the repo on every call.
|
|
290
|
+
"""
|
|
291
|
+
context = discover_local_repository(session_id, workspace_root, force=force_discovery)
|
|
292
|
+
lines = [
|
|
293
|
+
"You are a coding agent working directly in a real local repository via tool calls. "
|
|
294
|
+
"Verify with tools before claiming something is done or correct. Prefer minimal, "
|
|
295
|
+
"targeted changes over broad rewrites.",
|
|
296
|
+
f"Workspace root: {context['working_directory']}",
|
|
297
|
+
]
|
|
298
|
+
# discover_local_repository always sets repository_root (falling back to
|
|
299
|
+
# workspace_root itself when `git rev-parse --show-toplevel` fails) --
|
|
300
|
+
# `head` is the real "is this actually a Git repo" signal, since it's
|
|
301
|
+
# only None when that git call failed (see its `"no-head"` sentinel).
|
|
302
|
+
if context.get("head"):
|
|
303
|
+
lines.append(f"Git repository root: {context['repository_root']} branch: {context.get('branch') or '(detached HEAD)'}")
|
|
304
|
+
if context.get("dirty"):
|
|
305
|
+
lines.append(
|
|
306
|
+
f"The working tree already has {len(context.get('dirty_files') or [])} uncommitted change(s) -- "
|
|
307
|
+
"be careful not to conflate your own edits with pre-existing ones when reporting what changed."
|
|
308
|
+
)
|
|
309
|
+
else:
|
|
310
|
+
lines.append("This directory is not a Git repository.")
|
|
311
|
+
|
|
312
|
+
instruction_files = context.get("instruction_files") or []
|
|
313
|
+
remaining_budget = MAX_INSTRUCTION_CHARS
|
|
314
|
+
instruction_blocks = []
|
|
315
|
+
for path_str in instruction_files:
|
|
316
|
+
if remaining_budget <= 0:
|
|
317
|
+
break
|
|
318
|
+
try:
|
|
319
|
+
content = Path(path_str).read_text(encoding="utf-8", errors="ignore")
|
|
320
|
+
except OSError:
|
|
321
|
+
continue
|
|
322
|
+
snippet = content[:remaining_budget]
|
|
323
|
+
remaining_budget -= len(snippet)
|
|
324
|
+
truncated = " (truncated)" if len(snippet) < len(content) else ""
|
|
325
|
+
instruction_blocks.append(f"--- {path_str}{truncated} ---\n{snippet}")
|
|
326
|
+
if instruction_blocks:
|
|
327
|
+
lines.append("\nProject instructions found in this repository:\n" + "\n\n".join(instruction_blocks))
|
|
328
|
+
|
|
329
|
+
return "\n".join(lines)
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: tamfis-code
|
|
3
|
+
Version: 0.2.3
|
|
4
|
+
Summary: TamfisGPT Code -- standalone terminal coding agent (calls HF/NVIDIA NIM/OpenRouter/Ollama directly; --remote for the legacy TamfisGPT Remote backend)
|
|
5
|
+
Project-URL: Homepage, https://github.com/tamfitronics/tamfis-code
|
|
6
|
+
Project-URL: Repository, https://github.com/tamfitronics/tamfis-code
|
|
7
|
+
Requires-Python: >=3.10
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
Requires-Dist: click>=8.1
|
|
10
|
+
Requires-Dist: httpx>=0.27
|
|
11
|
+
Requires-Dist: rich>=13.0
|
|
12
|
+
Requires-Dist: prompt_toolkit>=3.0
|
|
13
|
+
Requires-Dist: openai>=1.30
|
|
14
|
+
Provides-Extra: dev
|
|
15
|
+
Requires-Dist: build>=1.0; extra == "dev"
|
|
16
|
+
Requires-Dist: twine>=5.0; extra == "dev"
|
|
17
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
18
|
+
Requires-Dist: pytest-asyncio>=0.24; extra == "dev"
|
|
19
|
+
|
|
20
|
+
# tamfis-code
|
|
21
|
+
|
|
22
|
+
A standalone terminal coding agent. By default it calls an LLM provider
|
|
23
|
+
directly — Hugging Face, NVIDIA NIM, OpenRouter, or Ollama — and runs its
|
|
24
|
+
own agent loop, tool execution, and local risk/approval/mutation-ledger
|
|
25
|
+
safety layer, with no separate backend process required.
|
|
26
|
+
|
|
27
|
+
`--remote` (or a persistent `default_backend = "remote"` config setting)
|
|
28
|
+
switches to the original architecture: a thin client to the TamfisGPT
|
|
29
|
+
Remote Workspace backend, for TamfisGPT tenants using their hosted account
|
|
30
|
+
the same way Codex CLI uses a ChatGPT/OpenAI account, kimi-code uses a Kimi
|
|
31
|
+
account, or Claude Code uses a Claude account.
|
|
32
|
+
|
|
33
|
+
## Install
|
|
34
|
+
|
|
35
|
+
```
|
|
36
|
+
pipx install tamfis-code
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
See [USAGE_INSTALL_RELEASE.md](./USAGE_INSTALL_RELEASE.md) for installing
|
|
40
|
+
from source, using a TamfisGPT tenancy, and the release process.
|
|
41
|
+
|
|
42
|
+
## Quick start
|
|
43
|
+
|
|
44
|
+
```
|
|
45
|
+
export OLLAMA_BASE_URL=http://localhost:11434/v1 # or set HF_TOKEN / NVIDIA_API_KEY / OPENROUTER_API_KEY
|
|
46
|
+
tamfis-code doctor # check provider connectivity
|
|
47
|
+
tamfis-code ask "explain what this repo does"
|
|
48
|
+
tamfis-code agent "add a health-check endpoint" # full read/write/execute loop
|
|
49
|
+
tamfis-code # interactive REPL
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Providers
|
|
53
|
+
|
|
54
|
+
| Provider | Env var | Notes |
|
|
55
|
+
|---|---|---|
|
|
56
|
+
| Ollama | `OLLAMA_BASE_URL` (default `http://localhost:11434/v1`) | Runs fully on-device, no API key |
|
|
57
|
+
| Hugging Face | `HF_TOKEN` | |
|
|
58
|
+
| NVIDIA NIM | `NVIDIA_API_KEY` | |
|
|
59
|
+
| OpenRouter | `OPENROUTER_API_KEY` | |
|
|
60
|
+
|
|
61
|
+
Select one explicitly with `--provider hf|nvidia|openrouter|ollama`, or
|
|
62
|
+
leave it as `auto` (default) to pick the best configured one.
|
|
63
|
+
|
|
64
|
+
## Safety model
|
|
65
|
+
|
|
66
|
+
Every mutating tool call (`write_file`, `edit_file`, `execute_command`) is
|
|
67
|
+
risk-classified locally (`tamfis_code/safety.py`), gated by
|
|
68
|
+
`--approval-policy` (or the `/mode` REPL command), and recorded in a local
|
|
69
|
+
mutation ledger — see `tamfis-code diffs` / `tamfis-code diff` /
|
|
70
|
+
`tamfis-code revert`. There is no sandboxing beyond risk classification;
|
|
71
|
+
review dangerous commands yourself before approving them.
|
|
72
|
+
|
|
73
|
+
## License
|
|
74
|
+
|
|
75
|
+
TBD — decide before the first public PyPI release (a public package still
|
|
76
|
+
needs a stated license; "all rights reserved"/proprietary is a valid choice
|
|
77
|
+
if you don't want redistribution/modification, but it should be explicit
|
|
78
|
+
rather than absent).
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
tamfis_code/__init__.py,sha256=3jsx8RkmYrqwqAy5VValfJC52IJ8UdfNq531s6erOOE,2175
|
|
2
|
+
tamfis_code/__main__.py,sha256=MSmt_5Xg84uHqzTN38JwgseJK8rsJn_11A8WD99VtEo,61
|
|
3
|
+
tamfis_code/agents.py,sha256=LGahD4L42zqL2BG_d81T7-0mga0dvkENxf07YwjyU38,14068
|
|
4
|
+
tamfis_code/api_client.py,sha256=kKiiM25giwYpttlxFm4uwvPox1iGsQ85-TGDVEYgAIE,19989
|
|
5
|
+
tamfis_code/cli.py,sha256=I7nX-MdEOZ-8Qyl7zVSKKEwMrW0_JmuvWYoZp6OzV5A,112492
|
|
6
|
+
tamfis_code/completion.py,sha256=qp_CWE4fNZCPSnN4MgbaLommso_RQfOg7EBP13bJu1I,4170
|
|
7
|
+
tamfis_code/config.py,sha256=5FH24a3xr4cPCKywHdpNRtMdy9nVgh7Uta0FaHgf8xQ,7642
|
|
8
|
+
tamfis_code/doctor.py,sha256=G0U73C5ZMsePJV5DqVSz0fL5vvv9vmVRfXefAONFInw,7848
|
|
9
|
+
tamfis_code/enforcer.py,sha256=RraE0YtETN-sk3T8vy1098ttYq9LExcdZJOU7mMzY2Y,12584
|
|
10
|
+
tamfis_code/indexer.py,sha256=lQWAWQK_eDkEYwiv4vCS9H81vm6MXytK7GhHwSSlxmg,20685
|
|
11
|
+
tamfis_code/instructions.py,sha256=iXL-28K8A2Or04OCfyT84yH342NW6DDgs3vpNGvvmos,3296
|
|
12
|
+
tamfis_code/interactive.py,sha256=x2WjPz-V0zuSDVyGGfyw92ZzjoHmtKieoxxk_axm1Pg,55034
|
|
13
|
+
tamfis_code/local_chat.py,sha256=s6A1avg5vLTxXyWgw8VRbf9tkD7ONa9gyTwP7thdjJ4,4860
|
|
14
|
+
tamfis_code/local_tools.py,sha256=ixQ7UZ0LzAGtRjAuhHwUAXDY7ZmnWFw-UaSYBvSzyWA,3594
|
|
15
|
+
tamfis_code/mcp.py,sha256=4RdGAQfZfQ9WO-1iqbINM9vycm7JzBLb5czpuKa8uIc,22230
|
|
16
|
+
tamfis_code/metrics.py,sha256=H-WEJVQ1n767hEx7t22Wq8GpUWAYUi78zUzC6JcxThM,3588
|
|
17
|
+
tamfis_code/providers.py,sha256=oFgsVAgiyGMHOjFt2cYAJLnbgw9B8LYqE9MMlJ_EY2w,15986
|
|
18
|
+
tamfis_code/references.py,sha256=gOG59W-hMw0xPklHSGdFoK28OGIQ3YRr4mdtwj25SNg,12864
|
|
19
|
+
tamfis_code/render.py,sha256=fgcHVkd5lyHt5osHaQwYaLgE7KvdkadzbCfmSehwi7Q,27016
|
|
20
|
+
tamfis_code/runner.py,sha256=yprlHDcMH7OdJCc1BZ5hMwO4vjG48wT2u-bRQl7gfw4,27751
|
|
21
|
+
tamfis_code/runner_local.py,sha256=G0hLGYdE0Hc432oRSZMWGF8UrCVRE0dLg3jsFyK9l80,18111
|
|
22
|
+
tamfis_code/safety.py,sha256=kgDfFJN_NSvtLs689tsoxtj5933oCwE3bs0Zd8B2q-I,7088
|
|
23
|
+
tamfis_code/screenshot.py,sha256=oHlnDIPmfRN713wA0hW_CP3kuDaICrxsfs4KwBj1FXM,13411
|
|
24
|
+
tamfis_code/sessions.py,sha256=N-Y3iA5pzp6-dFM8RYuVHY7wSJCQxOMH6AWT0ipKJEw,8833
|
|
25
|
+
tamfis_code/state.py,sha256=6kht9yxqKW2e7RXX8FoKSkYPXyIMD2_0xy3QnpTQqrQ,18215
|
|
26
|
+
tamfis_code/tasks.py,sha256=6BreOmRIEW5Qrb5HHdCmDCcBw2lr_J5z7H_BlPcaMB4,1630
|
|
27
|
+
tamfis_code/workspace.py,sha256=mCTpvtnvXGjCKBndLI9aY-PCzIrhmBqdCEIlyuZ_lv4,14383
|
|
28
|
+
tamfis_code-0.2.3.dist-info/METADATA,sha256=9ZzvdhWVRU_7zIqBYHq6CHWAXrM8rmn4sK6S3fhApFE,3084
|
|
29
|
+
tamfis_code-0.2.3.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
|
|
30
|
+
tamfis_code-0.2.3.dist-info/entry_points.txt,sha256=NctCDdh_oXnVhmrXaSJF93pFFDdt0qX93z1rAOI1PPI,118
|
|
31
|
+
tamfis_code-0.2.3.dist-info/top_level.txt,sha256=P9MDS_iv9fvIMpEbGmhbOnie8ifa40owxjCiq4sk_vg,12
|
|
32
|
+
tamfis_code-0.2.3.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
tamfis_code
|