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,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,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (83.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,4 @@
1
+ [console_scripts]
2
+ tamfis = tamfis_code.cli:main
3
+ tamfis-code = tamfis_code.cli:main
4
+ tamgpt-code = tamfis_code.cli:main
@@ -0,0 +1 @@
1
+ tamfis_code