Terminal-Assistant-Interface 1.0.6__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.
- termiai/__init__.py +230 -0
- termiai/__main__.py +8 -0
- termiai/clean.py +20 -0
- termiai_main.py +8 -0
- termilib/__init__.py +237 -0
- termilib/__main__.py +4 -0
- termilib/ftapi.py +229 -0
- termilib/icon_data.py +1 -0
- termilib/test.py +147 -0
- termilib/tools.py +372 -0
- terminal_assistant_interface-1.0.6.dist-info/METADATA +23 -0
- terminal_assistant_interface-1.0.6.dist-info/RECORD +16 -0
- terminal_assistant_interface-1.0.6.dist-info/WHEEL +5 -0
- terminal_assistant_interface-1.0.6.dist-info/entry_points.txt +3 -0
- terminal_assistant_interface-1.0.6.dist-info/licenses/LICENSE +21 -0
- terminal_assistant_interface-1.0.6.dist-info/top_level.txt +3 -0
termilib/tools.py
ADDED
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# Made by @Ericwasepic127
|
|
3
|
+
|
|
4
|
+
import httpx, json, asyncio, platformdirs, os, threading as t, uuid, time, datetime
|
|
5
|
+
|
|
6
|
+
CONFIGS = platformdirs.user_config_dir("TermiAI")
|
|
7
|
+
DATA = platformdirs.user_data_dir("TermiAI")
|
|
8
|
+
JOIN = os.path.join
|
|
9
|
+
LIST = os.listdir
|
|
10
|
+
CHATS = JOIN(CONFIGS, "chat.json")
|
|
11
|
+
APPCONFIG = None
|
|
12
|
+
CHATJSON = None
|
|
13
|
+
URL = None
|
|
14
|
+
BASEAIURL = "https://ai-proxy-%s.onrender.com/"
|
|
15
|
+
VERBOSE = False
|
|
16
|
+
THERROR = """<!doctype html>
|
|
17
|
+
<html lang=en>
|
|
18
|
+
<title>429 Too Many Requests</title>
|
|
19
|
+
<h1>Too Many Requests</h1>
|
|
20
|
+
<p>{"error": {"message": "Too many requests!"}}</p>"""
|
|
21
|
+
|
|
22
|
+
os.makedirs(CONFIGS, exist_ok=True)
|
|
23
|
+
os.makedirs(DATA, exist_ok=True)
|
|
24
|
+
|
|
25
|
+
def swrap(f):
|
|
26
|
+
def a(*a, **k):
|
|
27
|
+
res = f(*a, **k)
|
|
28
|
+
if VERBOSE and res:
|
|
29
|
+
try:
|
|
30
|
+
print("---", f.__name__, "---")
|
|
31
|
+
print(a, k)
|
|
32
|
+
print(res, repr(res), dir(res), type(res))
|
|
33
|
+
except:
|
|
34
|
+
...
|
|
35
|
+
return res
|
|
36
|
+
return a
|
|
37
|
+
|
|
38
|
+
def awrap(f):
|
|
39
|
+
async def a(*a, **k):
|
|
40
|
+
res = await f(*a, **k)
|
|
41
|
+
if VERBOSE and res:
|
|
42
|
+
try:
|
|
43
|
+
print("---", f.__name__, "---")
|
|
44
|
+
print(a, k)
|
|
45
|
+
print(res, repr(res), dir(res), type(res))
|
|
46
|
+
except:
|
|
47
|
+
...
|
|
48
|
+
return res
|
|
49
|
+
return a
|
|
50
|
+
|
|
51
|
+
@awrap
|
|
52
|
+
async def grab(url=None, method="GET", follow_redirects=True, **kwargs):
|
|
53
|
+
async with httpx.AsyncClient() as client:
|
|
54
|
+
return await getattr(client, method.lower())(url, follow_redirects=follow_redirects, **kwargs)
|
|
55
|
+
|
|
56
|
+
@awrap
|
|
57
|
+
async def ping(url=None):
|
|
58
|
+
if url is not None:
|
|
59
|
+
url = "https://%s-proxy-%s.onrender.com/" % url
|
|
60
|
+
else:
|
|
61
|
+
url = URL or BASEAIURL % "x"
|
|
62
|
+
return await grab(url, timeout=75)
|
|
63
|
+
|
|
64
|
+
@awrap
|
|
65
|
+
async def loop():
|
|
66
|
+
return await ping(), await ping(("search", "x"))
|
|
67
|
+
|
|
68
|
+
@awrap
|
|
69
|
+
async def get_current_time(local=True):
|
|
70
|
+
async with httpx.AsyncClient() as client:
|
|
71
|
+
try:
|
|
72
|
+
(1 / 0) if local else None
|
|
73
|
+
r = await client.get("https://currentdatetimepy.onrender.com/", timeout=10)
|
|
74
|
+
return r.text
|
|
75
|
+
except:
|
|
76
|
+
import datetime
|
|
77
|
+
return datetime.datetime.now(datetime.timezone.utc).ctime()
|
|
78
|
+
|
|
79
|
+
@awrap
|
|
80
|
+
async def run_command(command="", at="", cmd=""):
|
|
81
|
+
try:
|
|
82
|
+
process = await asyncio.create_subprocess_shell(
|
|
83
|
+
command or cmd,
|
|
84
|
+
stdout=asyncio.subprocess.PIPE,
|
|
85
|
+
stderr=asyncio.subprocess.PIPE,
|
|
86
|
+
stdin=asyncio.subprocess.DEVNULL,
|
|
87
|
+
cwd=at or None
|
|
88
|
+
)
|
|
89
|
+
stdout, stderr = await process.communicate()
|
|
90
|
+
return {"stdout": stdout.decode("utf-8", errors="replace"), "stderr": stderr.decode("utf-8", errors="replace"), "returncode": process.returncode}
|
|
91
|
+
except Exception as e:
|
|
92
|
+
return {"stdout": "", "stderr": getattr(type(e), "__name__", "<Unknown error>") + ": " + str(e), "returncode": 1}
|
|
93
|
+
|
|
94
|
+
@swrap
|
|
95
|
+
def createChatList():
|
|
96
|
+
if CHATJSON is None:
|
|
97
|
+
return []
|
|
98
|
+
give = []
|
|
99
|
+
for idcard, detail in CHATJSON.items():
|
|
100
|
+
give.append()
|
|
101
|
+
|
|
102
|
+
@swrap
|
|
103
|
+
def createChatName(default=None):
|
|
104
|
+
val = str(uuid.uuid4())
|
|
105
|
+
if CHATJSON is None:
|
|
106
|
+
return val
|
|
107
|
+
obj = JOIN(DATA, val + ".json")
|
|
108
|
+
CHATJSON[val] = ["New Chat", time.time()]
|
|
109
|
+
return val, LiveJson(obj, default_data=default)
|
|
110
|
+
|
|
111
|
+
@swrap
|
|
112
|
+
def loadChat(name):
|
|
113
|
+
return LiveJson(JOIN(DATA, name))
|
|
114
|
+
|
|
115
|
+
def append_message(role, message, var, raw=False):
|
|
116
|
+
var.append(message) if raw else var.append({"role": role, "content": message})
|
|
117
|
+
|
|
118
|
+
@awrap
|
|
119
|
+
async def post(data):
|
|
120
|
+
async with httpx.AsyncClient() as client:
|
|
121
|
+
try:
|
|
122
|
+
r = await client.post(URL or "https://ai-proxy-x.onrender.com/", content=json.dumps(data), headers={"Content-Type": "application/json"}, timeout=120)
|
|
123
|
+
m = r.json()
|
|
124
|
+
except json.JSONDecodeError as e:
|
|
125
|
+
try:
|
|
126
|
+
if ("<!doctype html>" in r.text and "<html>" in r.text and r.status_code == 429 and "error" in r.text) or THERROR in r.text.strip().rstrip():
|
|
127
|
+
m = {"error": {"message": "Too many requests per minute. Please wait about 1 minute"}}
|
|
128
|
+
else:
|
|
129
|
+
m = {"error": {"message": "DecodeError: " + r.text}}
|
|
130
|
+
except:
|
|
131
|
+
m = {"error": {"message": e.__class__.__name__ + ": " + str(e)}}
|
|
132
|
+
except (BaseException, Exception) as e:
|
|
133
|
+
m = {"error": {"message": e.__class__.__name__ + ": " + str(e)}}
|
|
134
|
+
return m
|
|
135
|
+
|
|
136
|
+
@awrap
|
|
137
|
+
async def instantSearch(**kwargs):
|
|
138
|
+
data = kwargs.get("q") or kwargs.get("query") or kwargs.get("prompt")
|
|
139
|
+
if not data:
|
|
140
|
+
return {"error": "No search query provided"}
|
|
141
|
+
async with httpx.AsyncClient() as client:
|
|
142
|
+
try:
|
|
143
|
+
r = await client.get("https://api.duckduckgo.com/", headers={"Content-Type": "application/json"}, timeout=120, params={"format": "json", "q": data})
|
|
144
|
+
m = r.json()
|
|
145
|
+
except json.JSONDecodeError:
|
|
146
|
+
m = {"error": {"message": "Decode failure on response JSON: " + r.text}}
|
|
147
|
+
except (BaseException, Exception) as e:
|
|
148
|
+
m = {"error": {"message": e.__class__.__name__ + ": " + str(e)}}
|
|
149
|
+
return m
|
|
150
|
+
|
|
151
|
+
@awrap
|
|
152
|
+
async def search(noredirectinsta=False, **kwargs):
|
|
153
|
+
data = kwargs.get("q") or kwargs.get("query") or kwargs.get("prompt")
|
|
154
|
+
count = int(kwargs.get("count", 5))
|
|
155
|
+
if not data:
|
|
156
|
+
return {"error": "No search query provided"}
|
|
157
|
+
if INSTANTSEARCH and not noredirectinsta:
|
|
158
|
+
return await instantSearch(q=data)
|
|
159
|
+
async with httpx.AsyncClient() as client:
|
|
160
|
+
try:
|
|
161
|
+
r = await client.post("https://search-proxy-x.onrender.com/", content=json.dumps({"query": data, "contents": {"text": True},"freshness": "noLimit", "count": count}), headers={"Content-Type": "application/json"}, timeout=120)
|
|
162
|
+
m = r.json()
|
|
163
|
+
except json.JSONDecodeError:
|
|
164
|
+
m = {"error": {"message": "Decode failure on response JSON: " + r.text}}
|
|
165
|
+
except (BaseException, Exception) as e:
|
|
166
|
+
m = {"error": {"message": e.__class__.__name__ + ": " + str(e)}}
|
|
167
|
+
return m
|
|
168
|
+
|
|
169
|
+
async def deepsearch(**kwargs):
|
|
170
|
+
return await search(**kwargs, noredirectinsta=True)
|
|
171
|
+
|
|
172
|
+
@swrap
|
|
173
|
+
def buildict(p, tools=None):
|
|
174
|
+
m = []
|
|
175
|
+
append_message("system", p, m)
|
|
176
|
+
r = {"messages": m}
|
|
177
|
+
if tools:
|
|
178
|
+
r["tools"] = tools
|
|
179
|
+
print(r)
|
|
180
|
+
return r
|
|
181
|
+
|
|
182
|
+
def deleteChat(name):
|
|
183
|
+
if name in CHATJSON or CHATJSON.get(name) is not None:
|
|
184
|
+
del CHATJSON[name]
|
|
185
|
+
file_path = JOIN(DATA, name + ".json")
|
|
186
|
+
if os.path.exists(file_path):
|
|
187
|
+
os.remove(file_path)
|
|
188
|
+
|
|
189
|
+
class LiveJson(dict):
|
|
190
|
+
def __init__(self, file_path="data.json", default_data=None):
|
|
191
|
+
self.file_path = file_path
|
|
192
|
+
self.lock = t.Lock()
|
|
193
|
+
|
|
194
|
+
initial_data = {}
|
|
195
|
+
if os.path.exists(self.file_path):
|
|
196
|
+
try:
|
|
197
|
+
with open(self.file_path, "r") as f:
|
|
198
|
+
initial_data = json.load(f)
|
|
199
|
+
except Exception:
|
|
200
|
+
initial_data = default_data or {}
|
|
201
|
+
else:
|
|
202
|
+
initial_data = default_data or {}
|
|
203
|
+
self._save_to_disk(initial_data)
|
|
204
|
+
|
|
205
|
+
super().__init__(initial_data)
|
|
206
|
+
|
|
207
|
+
def _save_to_disk(self, data):
|
|
208
|
+
try:
|
|
209
|
+
with open(self.file_path, "w") as f:
|
|
210
|
+
json.dump(data, f, indent=4)
|
|
211
|
+
except IOError:
|
|
212
|
+
pass
|
|
213
|
+
|
|
214
|
+
def __getitem__(self, key):
|
|
215
|
+
with self.lock:
|
|
216
|
+
return super().__getitem__(key)
|
|
217
|
+
|
|
218
|
+
def __setitem__(self, key, value):
|
|
219
|
+
with self.lock:
|
|
220
|
+
super().__setitem__(key, value)
|
|
221
|
+
current_state = dict(self.items())
|
|
222
|
+
self._save_to_disk(current_state)
|
|
223
|
+
|
|
224
|
+
def __delitem__(self, key):
|
|
225
|
+
with self.lock:
|
|
226
|
+
super().__delitem__(key)
|
|
227
|
+
current_state = dict(self.items())
|
|
228
|
+
self._save_to_disk(current_state)
|
|
229
|
+
|
|
230
|
+
def update(self, other=None, **kwargs):
|
|
231
|
+
with self.lock:
|
|
232
|
+
if other is not None:
|
|
233
|
+
if isinstance(other, dict):
|
|
234
|
+
for k, v in other.items():
|
|
235
|
+
if k in self and isinstance(self[k], dict) and isinstance(v, dict):
|
|
236
|
+
self[k].update(v)
|
|
237
|
+
else:
|
|
238
|
+
super().__setitem__(k, v)
|
|
239
|
+
else:
|
|
240
|
+
super().update(other)
|
|
241
|
+
if kwargs:
|
|
242
|
+
super().update(kwargs)
|
|
243
|
+
|
|
244
|
+
current_state = dict(self.items())
|
|
245
|
+
self._save_to_disk(current_state)
|
|
246
|
+
|
|
247
|
+
def reload(self):
|
|
248
|
+
with self.lock:
|
|
249
|
+
if os.path.exists(self.file_path):
|
|
250
|
+
try:
|
|
251
|
+
with open(self.file_path, "r") as f:
|
|
252
|
+
data = json.load(f)
|
|
253
|
+
super().clear()
|
|
254
|
+
super().update(data)
|
|
255
|
+
except Exception:
|
|
256
|
+
pass
|
|
257
|
+
def clone(self):
|
|
258
|
+
cloned = {}
|
|
259
|
+
for name, item in self.items():
|
|
260
|
+
cloned[name] = item
|
|
261
|
+
return cloned
|
|
262
|
+
|
|
263
|
+
class MemJson(dict):
|
|
264
|
+
def clone(self):
|
|
265
|
+
return MemJson(self)
|
|
266
|
+
|
|
267
|
+
class AsyncAI:
|
|
268
|
+
def __init__(self, jsonobj: LiveJson, sysprompt):
|
|
269
|
+
self.data = jsonobj
|
|
270
|
+
self.post = post
|
|
271
|
+
self.sysprompt = sysprompt
|
|
272
|
+
self.maxLimit = APPCONFIG.get("maxToolLoop", 4)
|
|
273
|
+
self.funcs = {}
|
|
274
|
+
self.tools = []
|
|
275
|
+
|
|
276
|
+
def removeTool(self, name):
|
|
277
|
+
if name in self.funcs:
|
|
278
|
+
self.tools = [t for t in self.tools if t["function"]["name"] != name]
|
|
279
|
+
del self.funcs[name]
|
|
280
|
+
|
|
281
|
+
def assignTool(self, asyncfunc, name, desc, params, needs=[]):
|
|
282
|
+
build = {"type": "function","function": {"parameters": {"type": "object","required": []}}}
|
|
283
|
+
build["function"]["name"] = name
|
|
284
|
+
build["function"]["description"] = desc
|
|
285
|
+
build["function"]["properties"] = params
|
|
286
|
+
build["function"]["required"] = needs
|
|
287
|
+
self.funcs[name] = asyncfunc
|
|
288
|
+
self.tools.append(build)
|
|
289
|
+
|
|
290
|
+
@awrap
|
|
291
|
+
async def send(self, m):
|
|
292
|
+
currentLoop = 0
|
|
293
|
+
dclon = {**self.data.clone(), "tools": self.tools} if self.tools else self.data.clone()
|
|
294
|
+
clone = [{"role": "system", "content": self.sysprompt}] + dclon.get("messages", [])
|
|
295
|
+
append_message("user", m, clone)
|
|
296
|
+
dclon["messages"] = clone
|
|
297
|
+
returned = await self.post(dclon)
|
|
298
|
+
c = returned.get("choices")
|
|
299
|
+
if not c:
|
|
300
|
+
append_message("assistant", returned.get("error").get("message"), clone)
|
|
301
|
+
self.data["messages"] = [c for c in clone if c.get("role") != "system"]
|
|
302
|
+
return returned.get("error").get("message")
|
|
303
|
+
airesponse = c[0]["message"]
|
|
304
|
+
append_message("", airesponse, clone, raw=True)
|
|
305
|
+
tool = c[0].get("finish_reason", "stop")
|
|
306
|
+
calls = airesponse.get("tool_calls", [])
|
|
307
|
+
while tool != "stop" and self.maxLimit > currentLoop and calls:
|
|
308
|
+
replica, coros = [], []
|
|
309
|
+
for call in calls:
|
|
310
|
+
fd = call.get("function")
|
|
311
|
+
func = self.funcs.get(fd.get("name"))
|
|
312
|
+
nm = fd.get("name")
|
|
313
|
+
arg = json.loads(fd.get("arguments"))
|
|
314
|
+
print(fd.get("name") + ":", arg) if VERBOSE else None
|
|
315
|
+
coros.append(func(**arg))
|
|
316
|
+
replica.append({"role": "tool", "tool_call_id": call.get("id")})
|
|
317
|
+
for index, res in enumerate(await asyncio.gather(*coros)):
|
|
318
|
+
replica[index]["content"] = res if isinstance(res, str) else (json.dumps(res) if isinstance(res, dict) else str(res))
|
|
319
|
+
for x in replica:
|
|
320
|
+
clone.append(x)
|
|
321
|
+
dclon["messages"] = clone
|
|
322
|
+
returned = await self.post(dclon)
|
|
323
|
+
c = returned.get("choices")
|
|
324
|
+
if not c:
|
|
325
|
+
append_message("assistant", returned.get("error").get("message"), clone)
|
|
326
|
+
self.data["messages"] = [c for c in clone if c.get("role") != "system"]
|
|
327
|
+
return returned.get("error").get("message")
|
|
328
|
+
airesponse = c[0]["message"]
|
|
329
|
+
append_message("", airesponse, clone, raw=True)
|
|
330
|
+
tool = c[0].get("finish_reason", "stop")
|
|
331
|
+
calls = airesponse.get("tool_calls", [])
|
|
332
|
+
currentLoop += 1
|
|
333
|
+
if tool == "tool_calls":
|
|
334
|
+
for call in calls:
|
|
335
|
+
clone.append({"role": "tool", "tool_call_id": call.get("id"), "content": "Too much tool calls: try again in next request - you cannot run any tool calls because you exceeding tool limit per one message"})
|
|
336
|
+
dclon["messages"] = clone
|
|
337
|
+
returned = await post(dclon)
|
|
338
|
+
c = returned.get("choices")
|
|
339
|
+
if not c:
|
|
340
|
+
tools.append_message("assistant", returned.get("error").get("message"), clone)
|
|
341
|
+
self.data["messages"] = [c for c in clone if c.get("role") != "system"]
|
|
342
|
+
return returned.get("error").get("message")
|
|
343
|
+
airesponse = c[0]["message"]
|
|
344
|
+
append_message("", airesponse, clone, raw=True)
|
|
345
|
+
self.data["messages"] = [c for c in clone if c.get("role") != "system"]
|
|
346
|
+
return airesponse.get("content") or airesponse
|
|
347
|
+
|
|
348
|
+
DEFAULTDATA = {
|
|
349
|
+
"dark": False,
|
|
350
|
+
"api": "x",
|
|
351
|
+
"search": True,
|
|
352
|
+
"custom": False,
|
|
353
|
+
"instantSearch": True,
|
|
354
|
+
"maxToolLoop": 4,
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
CHATJSON = LiveJson(CHATS)
|
|
358
|
+
APPCONFIG = LiveJson(JOIN(CONFIGS, "appcfg.json"), default_data=DEFAULTDATA)
|
|
359
|
+
URL = APPCONFIG.get("api", BASEAIURL % "x") if APPCONFIG.get("custom") else BASEAIURL % APPCONFIG.get("api", "x")
|
|
360
|
+
INSTANTSEARCH = APPCONFIG.get("instantSearch", True)
|
|
361
|
+
|
|
362
|
+
if __name__ == '__main__':
|
|
363
|
+
VERBOSE = True
|
|
364
|
+
asyncio.run(loop())
|
|
365
|
+
asyncio.run(deepsearch(q="What is capital of france"))
|
|
366
|
+
print(asyncio.run(post({"messages": [{"role": "user", "content": "Hi"}]})))
|
|
367
|
+
aichat = AsyncAI(LiveJson("test.json"), "Helpful AI Assistant called TermiAI")
|
|
368
|
+
aichat.assignTool(instantSearch, "ddgsearch", "Instant search API by duckduckgo, for quick lookups.", {"q": {"type": "string", "description": "Search query (no encode like test+example needed)"}}, ["q"])
|
|
369
|
+
aichat.assignTool(deepsearch, "deepsearch", "Deep search API by LangSearch, for deep and unfindable questions.", {"q": {"type": "string", "description": "Search query (no encode like test+example needed)"}, "count": {"type": "integer", "description": "Total counts"}}, ["q"])
|
|
370
|
+
aichat.assignTool(get_current_time, "currentutctime", "Get latest time", {"local": {"type": "boolean", "description": "Only set to False if you think local time is wrong (mostly no, so use true)"}}, ["local"])
|
|
371
|
+
aichat.assignTool(grab, "fetch", "Fetch things from given URL", {"url": {"type": "string", "description": "URL to fetch"}, "params": {"type": "object", "description": "Parameters"}}, ["url"])
|
|
372
|
+
print(asyncio.run(aichat.send("Hi! Can you get my local time clock in UTC please? And then grab latest Github News by that clock so u can fetch absalute latest!")))
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: Terminal-Assistant-Interface
|
|
3
|
+
Version: 1.0.6
|
|
4
|
+
Summary: Your personal AI assistant to help terminal
|
|
5
|
+
Author-email: Ericwasepic127 <ericwasepic127@outlook.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/ericwasepic127/termiai
|
|
8
|
+
Project-URL: Issues, https://github.com/ericwasepic127/termiai/issues
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Requires-Python: >=3.11
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
License-File: LICENSE
|
|
14
|
+
Requires-Dist: requests
|
|
15
|
+
Requires-Dist: platformdirs
|
|
16
|
+
Requires-Dist: httpx
|
|
17
|
+
Requires-Dist: flet[all]
|
|
18
|
+
Dynamic: license-file
|
|
19
|
+
|
|
20
|
+
### TermiAI
|
|
21
|
+
An AI client do terminal job just for you :)
|
|
22
|
+
### If you scared to type terminal commmand ...
|
|
23
|
+
Use This!! An AI will run commands just for you
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
termiai_main.py,sha256=VNIbUwpmv0BgmvRPwvw6OUThUtiZxDnuPRTKUCXAMso,134
|
|
2
|
+
termiai/__init__.py,sha256=y557UOxAN4RUClRn94sqV4VYcltthT7E8Ci8vGJ3c7U,52925
|
|
3
|
+
termiai/__main__.py,sha256=lTCaIM2LoD9rP9wlh5Z2HkCql5FYSYu72pfNKA0hWRo,132
|
|
4
|
+
termiai/clean.py,sha256=K89yLRHkeneyT4LxJhdkeIRDjhLFAclUoHpU-ave5aU,885
|
|
5
|
+
termilib/__init__.py,sha256=01Gl-B6Si0Gnexl0qIdfH8gmpOSZ5kjOw3kBLVxO2uQ,8609
|
|
6
|
+
termilib/__main__.py,sha256=gpQBHb5hd-1tOArTqbQ3MF7qmrsrymjEAbdD1hkNTHM,69
|
|
7
|
+
termilib/ftapi.py,sha256=_xMgbcNGmWp4MpPmC42Ty3AysuKo92L9SOSgY1pQ82s,8000
|
|
8
|
+
termilib/icon_data.py,sha256=nWzCHrg3OuWsbznwcJFmWrCubehTHwgDXgqgI3AxgMc,43911
|
|
9
|
+
termilib/test.py,sha256=oCIO1ln8GM0SI2dUHht_es6fl8cLSf1UcU1yMuh-Gew,4269
|
|
10
|
+
termilib/tools.py,sha256=z0lDPK9_ZKeWC8iyI7RslukPG6kAsSY8sEZWNvatcqg,14758
|
|
11
|
+
terminal_assistant_interface-1.0.6.dist-info/licenses/LICENSE,sha256=0Bl4Tc3Bc9GA-XopN9pIcteabWfnanUg2bWkc4LUS1o,1065
|
|
12
|
+
terminal_assistant_interface-1.0.6.dist-info/METADATA,sha256=sv3G30Xkoo1Ubbf-Xsb-apZ7uF1Pb1NSjuczkZwYHxA,797
|
|
13
|
+
terminal_assistant_interface-1.0.6.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
|
|
14
|
+
terminal_assistant_interface-1.0.6.dist-info/entry_points.txt,sha256=wKq2ohatv6trd8d3ycDq1lHt4qt5zgXn2OL4faT1crY,86
|
|
15
|
+
terminal_assistant_interface-1.0.6.dist-info/top_level.txt,sha256=X_BjXD8xIcYZ3sQudKvUb49g2T5gTxNjg6oL9Tzd5bo,30
|
|
16
|
+
terminal_assistant_interface-1.0.6.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Erkhemee
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|