exp-runtime-creator 0.6.0__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,271 @@
1
+ """Shared desktop/file authoring operations restricted to one workspace."""
2
+ from copy import deepcopy
3
+ from dataclasses import asdict, fields, is_dataclass
4
+ import hashlib
5
+ import json
6
+ import re
7
+ from pathlib import Path
8
+ import threading
9
+ import uuid
10
+
11
+ from .compiler import compile_details
12
+ from .model import (Project, ProjectError, atomic_write, project_from_record,
13
+ project_bytes, project_from_bytes, project_record, save_project, validate_structure)
14
+ from .sharing import guard, session_info, snapshot, request, digest
15
+
16
+
17
+ class Workspace:
18
+ def __init__(self, root):
19
+ self.root = Path(root).resolve(strict=True)
20
+ if not self.root.is_dir(): raise ProjectError('Workspace must be an existing directory')
21
+ self.lock = threading.RLock()
22
+ self.debuggers = {}
23
+ self.playtests = {}
24
+ self.responses = {}
25
+
26
+ def path(self, name, suffix=None):
27
+ path = (self.root / name).resolve()
28
+ if not path.is_relative_to(self.root) or path == self.root:
29
+ raise ProjectError('Paths must remain inside the configured workspace')
30
+ if suffix and path.suffix.lower() != suffix:
31
+ raise ProjectError(f'Expected a {suffix} file')
32
+ return path
33
+
34
+ def revision(self, path):
35
+ return hashlib.sha256(path.read_bytes()).hexdigest()
36
+
37
+ def read(self, name):
38
+ state = self.snapshot(name)
39
+ return state.project, state.revision
40
+
41
+ def snapshot(self, name):
42
+ return snapshot(self.path(name, '.expproj'))
43
+
44
+ def describe(self, name, offset=0, limit=100):
45
+ if type(offset) is not int or offset < 0 or type(limit) is not int or not 1 <= limit <= 200:
46
+ raise ProjectError('Use a nonnegative offset and limit from 1 to 200')
47
+ state = self.snapshot(name)
48
+ project = state.project
49
+ return {'revision': state.revision, 'live': state.live, 'unsaved': state.unsaved,
50
+ 'title': project.title, 'game': project.game,
51
+ 'pack_id': project.pack_id, 'episode_id': project.episode_id, 'start': project.start,
52
+ 'language': project.language, 'locales': project.locales, 'library': project.library,
53
+ 'native': bool(project.native), 'node_count': len(project.nodes),
54
+ 'nodes': [asdict(n) for n in project.nodes[offset:offset + limit]],
55
+ 'characters': [asdict(c) for c in project.characters],
56
+ 'assets': [{'id': a.id, 'name': a.name, 'embedded': a.data is not None,
57
+ 'bytes': len(a.data) if a.data else 0} for a in project.assets]}
58
+
59
+ def create(self, name, title='Untitled episode', game='shs', pack_id=30000, episode_id=1):
60
+ with self.lock:
61
+ path = self.path(name, '.expproj')
62
+ with guard(path):
63
+ if path.exists() or session_info(path): raise ProjectError('Project already exists')
64
+ project = Project(title=title, game=game, pack_id=pack_id, episode_id=episode_id)
65
+ save_project(project, path)
66
+ return self.describe(name)
67
+
68
+ def edit(self, name, revision, operation):
69
+ with self.lock:
70
+ project, actual = self.read(name)
71
+ if not revision or revision != actual:
72
+ raise ProjectError('Project changed; read its current revision before editing')
73
+ before = deepcopy(project)
74
+ operation(project)
75
+ validate_structure(project)
76
+ from .operations import check_removals
77
+ check_removals(before, project)
78
+ result = self.commit(name, revision, project_bytes(project, self.path(name)))
79
+ return dict(**result, changed=self.changes(before, project), library=project.library)
80
+
81
+ def commit(self, name, revision, data):
82
+ path = self.path(name, '.expproj')
83
+ project = project_from_bytes(data, path)
84
+ with guard(path):
85
+ info = session_info(path)
86
+ if info is None:
87
+ previous = path.read_bytes()
88
+ if digest(previous) != revision:
89
+ raise ProjectError('Project changed; read its current revision before editing')
90
+ checkpoint = self._checkpoint(name, previous, 'Before edit')
91
+ atomic_write(path, data)
92
+ return dict(revision=digest(data), undo_checkpoint=checkpoint['checkpoint'], live=False, unsaved=False)
93
+ # Do not hold a filesystem lock while the GUI processes a request. The
94
+ # GUI checks the revision again, including edits made after this read.
95
+ checkpoint = self.checkpoint(name, revision, 'Before edit')
96
+ result = request(info, 'commit', revision=revision, project=project_record(project))
97
+ return dict(**result, undo_checkpoint=checkpoint['checkpoint'])
98
+
99
+ @staticmethod
100
+ def changes(before, after):
101
+ def brief(value):
102
+ if is_dataclass(value): return brief(asdict(value))
103
+ if isinstance(value, bytes): return dict(bytes=len(value), sha256=hashlib.sha256(value).hexdigest())
104
+ if isinstance(value, str) and len(value) > 1000:
105
+ return dict(preview=value[:1000], characters=len(value), truncated=True)
106
+ if isinstance(value, list): return [brief(v) for v in value[:20]] + ([dict(truncated=True, total=len(value))] if len(value) > 20 else [])
107
+ if isinstance(value, dict):
108
+ keys = list(value)[:20]
109
+ result = {k: brief(value[k]) for k in keys}
110
+ if len(keys) < len(value): result['_truncated'] = dict(total=len(value))
111
+ return result
112
+ return value
113
+ result = {}
114
+ for field in fields(Project):
115
+ a, b = getattr(before, field.name), getattr(after, field.name)
116
+ if a == b: continue
117
+ if field.name in ('nodes', 'characters', 'assets'):
118
+ old, new = {r.id: r for r in a}, {r.id: r for r in b}
119
+ updated = sorted(key for key in new.keys() & old.keys() if new[key] != old[key])
120
+ result[field.name] = dict(added=sorted(new.keys() - old.keys()), removed=sorted(old.keys() - new.keys()),
121
+ updated=updated, details={key: {f.name: dict(before=brief(getattr(old[key], f.name)),
122
+ after=brief(getattr(new[key], f.name))) for f in fields(new[key])
123
+ if getattr(old[key], f.name) != getattr(new[key], f.name)} for key in updated[:20]})
124
+ if len(updated) > 20: result[field.name]['details_truncated'] = True
125
+ else:
126
+ result[field.name] = dict(before=brief(a), after=brief(b))
127
+ return result
128
+
129
+ def batch(self, name, revision, operations, *, preview=False):
130
+ from .operations import apply, check_removals
131
+ if not isinstance(operations, list) or not 1 <= len(operations) <= 200:
132
+ raise ProjectError('A transaction needs 1–200 operations')
133
+ def run(project):
134
+ for index, operation in enumerate(operations):
135
+ try: apply(self, project, operation)
136
+ except (KeyError, TypeError, ValueError) as error:
137
+ raise ProjectError(f'Operation {index}: {error}') from error
138
+ if not preview: return self.edit(name, revision, run)
139
+ with self.lock:
140
+ before, actual = self.read(name)
141
+ if revision != actual: raise ProjectError('Project changed; read its current revision before editing')
142
+ candidate = deepcopy(before); run(candidate)
143
+ validate_structure(candidate); check_removals(before, candidate)
144
+ from .diagnostics import diagnose
145
+ return dict(revision=actual, changed=self.changes(before, candidate), validation=diagnose(candidate), applied=False)
146
+
147
+ def _history(self, name):
148
+ identity = str(self.path(name, '.expproj').relative_to(self.root))
149
+ return self.path('.creator-history/' + hashlib.sha256(identity.encode()).hexdigest() + '/index.json')
150
+
151
+ def checkpoints(self, name):
152
+ with guard(self.path(name, '.expproj')):
153
+ return self._checkpoints(name)
154
+
155
+ def _checkpoints(self, name):
156
+ index = self._history(name)
157
+ if not index.exists(): return []
158
+ records = json.loads(index.read_text(encoding='utf-8'))
159
+ if (not isinstance(records, list) or len(records) > 40 or any(
160
+ not isinstance(r, dict) or set(r) != {'checkpoint', 'revision', 'label'} or
161
+ not isinstance(r['checkpoint'], str) or not re.fullmatch('[0-9a-f]{32}', r['checkpoint']) or
162
+ not isinstance(r['revision'], str) or not re.fullmatch('[0-9a-f]{64}', r['revision']) or
163
+ not isinstance(r['label'], str) for r in records)):
164
+ raise ProjectError('Invalid checkpoint index')
165
+ return records
166
+
167
+ def checkpoint(self, name, revision, label='Checkpoint'):
168
+ with self.lock:
169
+ state = self.snapshot(name)
170
+ if revision != state.revision: raise ProjectError('Project changed; reread before checkpointing')
171
+ with guard(self.path(name, '.expproj')):
172
+ return self._checkpoint(name, state.data, label)
173
+
174
+ def _checkpoint(self, name, data, label):
175
+ if not isinstance(label, str) or len(label) > 200: raise ProjectError('Checkpoint label must fit 200 characters')
176
+ index = self._history(name)
177
+ index.parent.mkdir(parents=True, exist_ok=True)
178
+ records = self._checkpoints(name)
179
+ key = uuid.uuid4().hex
180
+ target = self.path(index.parent / (key + '.expproj'))
181
+ atomic_write(target, data)
182
+ record = dict(checkpoint=key, revision=digest(data), label=label)
183
+ records.append(record)
184
+ atomic_write(index, json.dumps(records[-40:]).encode())
185
+ for old in records[:-40]:
186
+ self.path(index.parent / (old['checkpoint'] + '.expproj')).unlink(missing_ok=True)
187
+ return record
188
+
189
+ def restore(self, name, revision, checkpoint):
190
+ with self.lock:
191
+ target = self.path(name, '.expproj')
192
+ record = next((r for r in self.checkpoints(name) if r['checkpoint'] == checkpoint), None)
193
+ if record is None: raise ProjectError('Unknown checkpoint for this project')
194
+ source = self.path(self._history(name).parent / (checkpoint + '.expproj'))
195
+ if self.revision(source) != record['revision']: raise ProjectError('Checkpoint checksum does not match')
196
+ # Raw bytes retain library paths relative to the original project location.
197
+ data = source.read_bytes()
198
+ project_from_record(json.loads(data))
199
+ result = self.commit(name, revision, data)
200
+ return dict(**result, restored=checkpoint)
201
+
202
+ def update(self, name, revision, changes):
203
+ return self.batch(name, revision, [dict(op='project', changes=changes)])
204
+
205
+ def node(self, name, revision, node_id, changes=None, remove=False):
206
+ return self.batch(name, revision, [dict(op='node', id=node_id, changes=changes or {}, remove=remove)])
207
+
208
+ def character(self, name, revision, character_id, changes=None, remove=False):
209
+ return self.batch(name, revision, [dict(op='character', id=character_id, changes=changes or {}, remove=remove)])
210
+
211
+ def import_image(self, name, revision, source):
212
+ from .model import MAX_ASSET_BYTES
213
+ path = self.path(source)
214
+ if path.stat().st_size > MAX_ASSET_BYTES: raise ProjectError('Image is too large')
215
+ return self.edit(name, revision, lambda project: project.add_image(path.name, path.read_bytes()))
216
+
217
+ def library_image(self, name, revision, library, resource, episode=None):
218
+ from exp_runtime.content import ContentLibrary
219
+ from .assets import import_library_image
220
+ path = self.path(library)
221
+ def apply(project):
222
+ with ContentLibrary(path) as content:
223
+ import_library_image(project, content, resource, content.open_episode(episode) if episode else None)
224
+ return self.edit(name, revision, apply)
225
+
226
+ def import_episode(self, source, destination, game='shs'):
227
+ from .native import import_exp
228
+ from .model import MAX_PROJECT_BYTES
229
+ with self.lock:
230
+ source, output = self.path(source, '.exp'), self.path(destination, '.expproj')
231
+ if source.stat().st_size > MAX_PROJECT_BYTES: raise ProjectError('EXP exceeds the editable project size limit')
232
+ with guard(output):
233
+ if output.exists() or session_info(output): raise ProjectError('Project destination already exists')
234
+ save_project(import_exp(source.read_bytes(), game=game), output)
235
+ return self.describe(destination)
236
+
237
+ def export(self, name, destination, revision, language=None, replace_revision=None):
238
+ with self.lock:
239
+ project, actual = self.read(name)
240
+ if actual != revision: raise ProjectError('Project changed; read its current revision before exporting')
241
+ output = self.path(destination, '.exp')
242
+ if output.exists() and self.revision(output) != replace_revision:
243
+ raise ProjectError('Export exists; provide its SHA-256 as replace_revision to replace it')
244
+ result = compile_details(project, language=language)
245
+ atomic_write(output, result.data)
246
+ return {'path': str(output.relative_to(self.root)), 'sha256': self.revision(output),
247
+ 'bytes': len(result.data), 'scripts': sorted(result.locations)}
248
+
249
+ def start_debug(self, name, start=None, *, seed=None, library=None, language=None):
250
+ from .debugger import DebugSession
251
+ if len(self.debuggers) >= 8: raise ProjectError('Close a debug session before starting another (limit 8)')
252
+ project, revision = self.read(name)
253
+ if library is not None: project.library = str(self.path(library)) if library else ''
254
+ if project.library: project.library = str(self.path(project.library))
255
+ debugger = DebugSession(project, start=start, seed=seed, library=project.library or None, language=language)
256
+ key = uuid.uuid4().hex
257
+ debugger.path, debugger.revision = name, revision
258
+ self.debuggers[key] = debugger
259
+ from .playtest import Playtest
260
+ self.playtests[key] = Playtest(debugger)
261
+ return {'session': key, 'revision': revision, **debugger.state()}
262
+
263
+ def debugger(self, key):
264
+ if key not in self.debuggers: raise ProjectError('Unknown or closed playtest session')
265
+ return self.debuggers[key]
266
+
267
+ def close_debug(self, key):
268
+ debugger = self.debuggers.pop(key, None)
269
+ if debugger is not None: debugger.close()
270
+ self.playtests.pop(key, None)
271
+ return dict(closed=True)
@@ -0,0 +1,75 @@
1
+ Metadata-Version: 2.4
2
+ Name: exp-runtime-creator
3
+ Version: 0.6.0
4
+ Summary: Visual desktop episode creator and native EXP compiler for EXP Runtime
5
+ License: MIT License
6
+
7
+ Copyright (c) 2025 Josh Mcdaniel
8
+
9
+ Permission is hereby granted, free of charge, to any person obtaining a copy
10
+ of this software and associated documentation files (the "Software"), to deal
11
+ in the Software without restriction, including without limitation the rights
12
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
13
+ copies of the Software, and to permit persons to whom the Software is
14
+ furnished to do so, subject to the following conditions:
15
+
16
+ The above copyright notice and this permission notice shall be included in all
17
+ copies or substantial portions of the Software.
18
+
19
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
25
+ SOFTWARE.
26
+
27
+ Requires-Python: >=3.10
28
+ Description-Content-Type: text/markdown
29
+ License-File: LICENSE
30
+ Requires-Dist: exp-runtime-formats==0.6.0
31
+ Provides-Extra: mcp
32
+ Requires-Dist: mcp<2,>=1.28; extra == "mcp"
33
+ Requires-Dist: exp-runtime-player[desktop]==0.6.0; extra == "mcp"
34
+ Requires-Dist: exp-runtime-shs==0.6.0; extra == "mcp"
35
+ Requires-Dist: exp-runtime-cod==0.6.0; extra == "mcp"
36
+ Provides-Extra: desktop
37
+ Requires-Dist: PySide6-Essentials<7,>=6.10; extra == "desktop"
38
+ Requires-Dist: exp-runtime-player[desktop]==0.6.0; extra == "desktop"
39
+ Requires-Dist: exp-runtime-shs==0.6.0; extra == "desktop"
40
+ Requires-Dist: exp-runtime-cod==0.6.0; extra == "desktop"
41
+ Dynamic: license-file
42
+
43
+ # EXP Runtime Creator
44
+
45
+ Optional visual desktop editor for new SHS and CoD episodes. Author dialogue,
46
+ choices, branches, characters, audio, minigames and translations, save portable projects and export native CSPUD/KiWi EXP
47
+ files for EXP Runtime and the original games. Original-slot export preserves
48
+ an existing episode's catalog IDs. Native KiWi loader/VM checks pass for SHS
49
+ Android, SHS iOS and CoD iOS; full original-app playback remains unverified.
50
+
51
+ Install the desktop editor with pip, then launch it:
52
+
53
+ ```sh
54
+ python -m pip install 'exp-runtime-creator[desktop]>=0.6.0'
55
+ exp-creator
56
+ ```
57
+
58
+ Standalone Creator downloads are also included in the v0.6.0 GitHub release
59
+ for Windows, Linux and macOS (Apple Silicon and Intel).
60
+
61
+ The Python authoring API needs only `exp-runtime-formats`. The `desktop` extra
62
+ adds Qt, the shared player and both game adapters. No game assets are included.
63
+ Preview uses an imported game library and temporary saves. The independent
64
+ `mcp` extra adds a stdio server, production playtesting and isolated player
65
+ rendering/audio previews. It includes pygame but does not install Qt.
66
+ Run `exp-creator-mcp --workspace /path/to/stories`.
67
+
68
+ Install both extras with `python -m pip install 'exp-runtime-creator[desktop,mcp]>=0.6.0'`
69
+ to use the editor and MCP server together.
70
+
71
+ Open a saved workspace project in the desktop creator to share live edits with
72
+ an MCP client. Models see unsaved work, their edits appear immediately, and
73
+ desktop undo/redo covers both. Save in the editor to write the shared project.
74
+
75
+ See the project README and `docs/CREATOR.md` in the source repository.
@@ -0,0 +1,34 @@
1
+ exp_runtime/creator/__init__.py,sha256=QZLWZm30iREFtLVJTGqsVkfaqXQftp96gwmmjipgoBU,516
2
+ exp_runtime/creator/__main__.py,sha256=XswRWYtj0jgiQbgG7O9D6Q7Fd5TqRrTPA_XQTAt7O1o,3305
3
+ exp_runtime/creator/assets.py,sha256=LKCY1seEcpVJfqPLHlZVmZlcjVjJ3XF5CVMWiDoFSVo,2296
4
+ exp_runtime/creator/catalog.py,sha256=wkwF_WftOQTWRjQEYDd2XLw7YgDvr4mEDpg5sOqp6S4,1630
5
+ exp_runtime/creator/compiler.py,sha256=5v-1CEN0G-0Kwq30GW9qRCupnBzPJOD0yfceYlhexF4,23490
6
+ exp_runtime/creator/debugger.py,sha256=Re2sAjhYbCRo3Y1ZVqVaXcIMzg43oou6KuOWNIsVxuw,5616
7
+ exp_runtime/creator/diagnostics.py,sha256=hEI4vj17J9g__UBdPX9U1NhUJ4YIYPiDRC0Gy95A2NA,5850
8
+ exp_runtime/creator/editing.py,sha256=mSonHvyqckzc2vQHiyZMs5akBWm3yB339msBDUqeXnk,3582
9
+ exp_runtime/creator/expressions.py,sha256=OytGo-IRfJhWPGg1L_IDletiheGlpT6u8EKDAnulMFg,3916
10
+ exp_runtime/creator/features.py,sha256=DI2xwXLonw4Ltojqnu9CteCkN53vJIITQFUlQ7KPx3U,5259
11
+ exp_runtime/creator/graph.py,sha256=kZuUNSDiiN5DVpJY4U90AGiTVLgH07X0Ia2Nr_EXv9I,6424
12
+ exp_runtime/creator/kiwi-hammer.svg,sha256=6c8OU7GDOaBSrFl2kRHCRrfHTrIXh6UEBfd2wvqo0mw,6179
13
+ exp_runtime/creator/live.py,sha256=Vf41PrHrHVxmFEdlbN8iadlBUZNgro8137BSIH0Z3ec,6039
14
+ exp_runtime/creator/mcp_results.py,sha256=Kzqj8oKwEwf6Re66C1OJAquHNUwxrEl8TbRfJ_7Vf5s,3091
15
+ exp_runtime/creator/mcp_server.py,sha256=fzwDizrfvT_pr03O5Y6bVRCZ_2HhiywkrMBnsG00c8E,26203
16
+ exp_runtime/creator/mcp_types.py,sha256=ARlAVAttrD2qXcYbcPJMdfjCCOPKAdJEOA6KQQFpyv8,3481
17
+ exp_runtime/creator/media.py,sha256=gw1WoIvkOZEPFogEbzlhYdPgWwyHs0x1w5NRNGqJ8lA,6983
18
+ exp_runtime/creator/minigames.py,sha256=IzmL0H1XHyu-KnMDI8t4xnM45x27WP3iaAy2NFoIx2g,10668
19
+ exp_runtime/creator/model.py,sha256=p5Ewosfy9ueGHqfzPD9mxh8kYFNkz4oFm3OYSmN9NLY,15903
20
+ exp_runtime/creator/native.py,sha256=P4yRG3xyEyX1Y_ifE4v7XgUWZcj-LWa5_odbKSjsTqY,8978
21
+ exp_runtime/creator/operations.py,sha256=6LAXZuuzGZvyyIuLtLoCKnun81MiLbFOvQOy1RtZYHc,4973
22
+ exp_runtime/creator/panels.py,sha256=vM31pl5Vpkjh7fNS2ZKodIYTznj2FZfj3eKvh-_b-DQ,11719
23
+ exp_runtime/creator/playtest.py,sha256=37o4q4rwVcBJasrdaT0iIwQYG-mowaqyp-x1ZUM-r1U,10939
24
+ exp_runtime/creator/preview.py,sha256=Ld-b4ur8S3-3HoFxiNgciN7iTQt_zzALMkoD1wd6fW4,1248
25
+ exp_runtime/creator/schema.py,sha256=qomZ89c8tpAekNf6mlCXAsVaQ6rSqqzyiIv7d4RnxTo,7694
26
+ exp_runtime/creator/sharing.py,sha256=lQy5pLSQwfa6OoB38-gKq_GGgGg2qFAn1NNF_jazVF4,6450
27
+ exp_runtime/creator/ui.py,sha256=hXsXaaRHWfkdba0nLp6riAMCAzCMYB6z5mUEcgdN9as,51992
28
+ exp_runtime/creator/workspace.py,sha256=tMOtPQWahoz41Uo0onOmhzWJgmNEobG8t1JCRidjFAc,14718
29
+ exp_runtime_creator-0.6.0.dist-info/licenses/LICENSE,sha256=y2xxdaMMD9iKqLFR2Lp8sM_0b3qZWxU4_vW0zgufh2k,1070
30
+ exp_runtime_creator-0.6.0.dist-info/METADATA,sha256=ZnO1QgWDL581sghccDFs7nLPF4swD9mdlMyIm4y7lJI,3652
31
+ exp_runtime_creator-0.6.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
32
+ exp_runtime_creator-0.6.0.dist-info/entry_points.txt,sha256=d9mJXJursuv0dw3lpULLwZ_phfaJT5sjTk_5C_HDkn4,135
33
+ exp_runtime_creator-0.6.0.dist-info/top_level.txt,sha256=1uRgkxqLAgYpITLluWh9qs6TpAD83zFoh0E5lbic4RA,12
34
+ exp_runtime_creator-0.6.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,5 @@
1
+ [console_scripts]
2
+ exp-creator-mcp = exp_runtime.creator.mcp_server:main
3
+
4
+ [gui_scripts]
5
+ exp-creator = exp_runtime.creator.__main__:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Josh Mcdaniel
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.
@@ -0,0 +1 @@
1
+ exp_runtime