spriteforge 0.1.1__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.
Files changed (218) hide show
  1. spriteforge/__init__.py +1 -0
  2. spriteforge/__main__.py +3 -0
  3. spriteforge/agents/__init__.py +0 -0
  4. spriteforge/agents/assets.py +109 -0
  5. spriteforge/agents/cancellation.py +12 -0
  6. spriteforge/agents/director.py +50 -0
  7. spriteforge/agents/models.py +63 -0
  8. spriteforge/agents/roles/art-director.md +15 -0
  9. spriteforge/agents/roles/art-reviewer.md +14 -0
  10. spriteforge/agents/roles/director.md +14 -0
  11. spriteforge/agents/roles/game-designer.md +15 -0
  12. spriteforge/agents/roles/level-tile-designer.md +16 -0
  13. spriteforge/agents/roles/librarian.md +13 -0
  14. spriteforge/agents/roles/menu-designer.md +15 -0
  15. spriteforge/agents/roles/pixel-artist.md +14 -0
  16. spriteforge/agents/roles/programmer.md +14 -0
  17. spriteforge/agents/roles/qa-playtester.md +13 -0
  18. spriteforge/agents/roles/sound-director.md +14 -0
  19. spriteforge/agents/roles.py +39 -0
  20. spriteforge/agents/router.py +233 -0
  21. spriteforge/agents/router_eval.py +145 -0
  22. spriteforge/agents/routing_state.py +133 -0
  23. spriteforge/agents/runtime.py +430 -0
  24. spriteforge/agents/workflow.py +389 -0
  25. spriteforge/app/__init__.py +0 -0
  26. spriteforge/app/agents_view.py +248 -0
  27. spriteforge/app/chat.py +114 -0
  28. spriteforge/app/extensions.py +115 -0
  29. spriteforge/app/index.html +279 -0
  30. spriteforge/app/local_only.py +49 -0
  31. spriteforge/app/server.py +358 -0
  32. spriteforge/app/static/assets/index-L2s1wlcp.css +1 -0
  33. spriteforge/app/static/assets/index-aBfzgLQl.js +71 -0
  34. spriteforge/app/static/assets/press-start-2p-latin-400-normal-BKfmdmKK.woff +0 -0
  35. spriteforge/app/static/assets/press-start-2p-latin-400-normal-xj9lRvVs.woff2 +0 -0
  36. spriteforge/app/static/index.html +13 -0
  37. spriteforge/app/static/press-start-2p-LICENSE.txt +93 -0
  38. spriteforge/app/studio.py +278 -0
  39. spriteforge/app/workers.py +73 -0
  40. spriteforge/cli.py +241 -0
  41. spriteforge/config.py +193 -0
  42. spriteforge/config.yaml +157 -0
  43. spriteforge/core/__init__.py +0 -0
  44. spriteforge/core/acting.py +16 -0
  45. spriteforge/core/cost.py +236 -0
  46. spriteforge/core/credentials.py +24 -0
  47. spriteforge/core/demo.py +79 -0
  48. spriteforge/core/forms.py +20 -0
  49. spriteforge/core/gates.py +175 -0
  50. spriteforge/core/security.py +42 -0
  51. spriteforge/core/state.py +222 -0
  52. spriteforge/core/team.py +15 -0
  53. spriteforge/core/usage.py +68 -0
  54. spriteforge/game/__init__.py +0 -0
  55. spriteforge/game/actors.py +377 -0
  56. spriteforge/game/agent_tools.py +178 -0
  57. spriteforge/game/art.py +2097 -0
  58. spriteforge/game/art_checks.py +190 -0
  59. spriteforge/game/art_review.py +518 -0
  60. spriteforge/game/build.py +134 -0
  61. spriteforge/game/bundle.py +1178 -0
  62. spriteforge/game/cli.py +119 -0
  63. spriteforge/game/codegen.py +433 -0
  64. spriteforge/game/codeinfo.py +154 -0
  65. spriteforge/game/dbsource.py +137 -0
  66. spriteforge/game/design.py +342 -0
  67. spriteforge/game/export.py +62 -0
  68. spriteforge/game/gamedb.py +191 -0
  69. spriteforge/game/image_cache.py +61 -0
  70. spriteforge/game/lessons.py +259 -0
  71. spriteforge/game/level_gen.py +118 -0
  72. spriteforge/game/level_preview.py +97 -0
  73. spriteforge/game/levels.py +934 -0
  74. spriteforge/game/loops.py +38 -0
  75. spriteforge/game/menus.py +712 -0
  76. spriteforge/game/mock_game.js +109 -0
  77. spriteforge/game/mock_platformer.js +58 -0
  78. spriteforge/game/motion.py +881 -0
  79. spriteforge/game/pipeline.py +1768 -0
  80. spriteforge/game/plan.py +502 -0
  81. spriteforge/game/playtest.py +318 -0
  82. spriteforge/game/qa.py +664 -0
  83. spriteforge/game/qa_checks.py +223 -0
  84. spriteforge/game/qa_runtime.py +565 -0
  85. spriteforge/game/reach.py +807 -0
  86. spriteforge/game/record.py +138 -0
  87. spriteforge/game/research.py +262 -0
  88. spriteforge/game/revision.py +225 -0
  89. spriteforge/game/sdk/API.md +104 -0
  90. spriteforge/game/sdk/FLOW.md +19 -0
  91. spriteforge/game/sdk/LEVELS.md +65 -0
  92. spriteforge/game/sdk/sf.js +418 -0
  93. spriteforge/game/sdk/sfflow.js +442 -0
  94. spriteforge/game/sdk/sflevel.js +683 -0
  95. spriteforge/game/sdk/sfsound-editor.js +225 -0
  96. spriteforge/game/sdk/sfsound.js +260 -0
  97. spriteforge/game/sdk/sounds.html +67 -0
  98. spriteforge/game/sounds.py +793 -0
  99. spriteforge/game/story.py +188 -0
  100. spriteforge/game/structure.py +98 -0
  101. spriteforge/game/team_reports.py +394 -0
  102. spriteforge/game/tilesets.py +70 -0
  103. spriteforge/game/title_art.py +186 -0
  104. spriteforge/genres/__init__.py +0 -0
  105. spriteforge/library/__init__.py +15 -0
  106. spriteforge/library/cli.py +126 -0
  107. spriteforge/library/describe.py +242 -0
  108. spriteforge/library/looks.py +154 -0
  109. spriteforge/library/models.py +79 -0
  110. spriteforge/library/store.py +422 -0
  111. spriteforge/library/tilesets/README.md +32 -0
  112. spriteforge/library/tilesets/gandalf-seasons.json +111 -0
  113. spriteforge/library/tilesets/pixel-crawler-dungeon.json +66 -0
  114. spriteforge/mcp/__init__.py +0 -0
  115. spriteforge/mcp/cli.py +58 -0
  116. spriteforge/mcp/client.py +57 -0
  117. spriteforge/mcp/filesystem_read.py +32 -0
  118. spriteforge/mcp/gamedb.py +56 -0
  119. spriteforge/mcp/library.py +179 -0
  120. spriteforge/mcp/memory.py +37 -0
  121. spriteforge/mcp/pixel_tools.py +145 -0
  122. spriteforge/mcp/registry.py +145 -0
  123. spriteforge/mcp/service.py +224 -0
  124. spriteforge/mcp/templates/context7.json +6 -0
  125. spriteforge/mcp/templates/fetch.json +6 -0
  126. spriteforge/mcp/templates/filesystem-read.json +6 -0
  127. spriteforge/mcp/templates/pixel-tools.json +6 -0
  128. spriteforge/memory/README.md +10 -0
  129. spriteforge/memory/__init__.py +0 -0
  130. spriteforge/memory/cli.py +131 -0
  131. spriteforge/memory/okf/__init__.py +1 -0
  132. spriteforge/memory/okf/catalog.py +331 -0
  133. spriteforge/memory/okf/codec.py +63 -0
  134. spriteforge/memory/okf/git.py +104 -0
  135. spriteforge/memory/okf/indexes.py +50 -0
  136. spriteforge/memory/okf/models.py +114 -0
  137. spriteforge/memory/okf/search.py +53 -0
  138. spriteforge/memory/okf/seed.py +81 -0
  139. spriteforge/memory/okf/seeds/genres.yaml +121 -0
  140. spriteforge/memory/okf/seeds/other.yaml +150 -0
  141. spriteforge/memory/okf/store.py +402 -0
  142. spriteforge/memory/okf/validate.py +115 -0
  143. spriteforge/pixel/__init__.py +17 -0
  144. spriteforge/pixel/atlas.py +110 -0
  145. spriteforge/pixel/grid.py +174 -0
  146. spriteforge/pixel/palette.py +156 -0
  147. spriteforge/pixel/palettes/dungeon.json +9 -0
  148. spriteforge/pixel/postprocess.py +170 -0
  149. spriteforge/pixel/preview.py +40 -0
  150. spriteforge/pixel/schemas.py +139 -0
  151. spriteforge/pixel/source.py +237 -0
  152. spriteforge/pixel/terrain.py +48 -0
  153. spriteforge/pixel/validate.py +138 -0
  154. spriteforge/prompts/anim_frame.txt +1 -0
  155. spriteforge/prompts/character.txt +1 -0
  156. spriteforge/prompts/item.txt +1 -0
  157. spriteforge/prompts/tile.txt +1 -0
  158. spriteforge/prompts/ui.txt +1 -0
  159. spriteforge/providers/__init__.py +0 -0
  160. spriteforge/providers/base.py +97 -0
  161. spriteforge/providers/http.py +38 -0
  162. spriteforge/providers/mock.py +111 -0
  163. spriteforge/providers/muse_image.py +60 -0
  164. spriteforge/providers/muse_search.py +91 -0
  165. spriteforge/providers/muse_spark.py +66 -0
  166. spriteforge/skills/__init__.py +0 -0
  167. spriteforge/skills/builtin/LICENSE-awesome-gamedev-agent-skills.txt +201 -0
  168. spriteforge/skills/builtin/NOTICE-awesome-gamedev-agent-skills.txt +15 -0
  169. spriteforge/skills/builtin/THIRD_PARTY.md +21 -0
  170. spriteforge/skills/builtin/audio-design/SKILL.md +190 -0
  171. spriteforge/skills/builtin/game-feel/SKILL.md +174 -0
  172. spriteforge/skills/builtin/game-ui-ux/SKILL.md +167 -0
  173. spriteforge/skills/builtin/level-design/SKILL.md +169 -0
  174. spriteforge/skills/builtin/phaser-arcade-physics/SKILL.md +17 -0
  175. spriteforge/skills/builtin/physics-tuning/SKILL.md +164 -0
  176. spriteforge/skills/builtin/pixel-art-prompting/SKILL.md +12 -0
  177. spriteforge/skills/builtin/platformer/SKILL.md +153 -0
  178. spriteforge/skills/builtin/puzzle/SKILL.md +143 -0
  179. spriteforge/skills/builtin/sprite-animation-frames/SKILL.md +11 -0
  180. spriteforge/skills/builtin/tileset-design/SKILL.md +10 -0
  181. spriteforge/skills/catalog.py +200 -0
  182. spriteforge/skills/cli.py +41 -0
  183. spriteforge/stages/__init__.py +0 -0
  184. spriteforge/stages/asset_context.py +301 -0
  185. spriteforge/stages/asset_models.py +192 -0
  186. spriteforge/stages/asset_render.py +366 -0
  187. spriteforge/stages/asset_review.py +140 -0
  188. spriteforge/stages/asset_store.py +94 -0
  189. spriteforge/stages/chat_changes.py +236 -0
  190. spriteforge/stages/concept.py +42 -0
  191. spriteforge/stages/game.py +1699 -0
  192. spriteforge/stages/game_confirm.py +112 -0
  193. spriteforge/stages/game_final.py +220 -0
  194. spriteforge/stages/game_record.py +246 -0
  195. spriteforge/stages/gameplay.py +218 -0
  196. spriteforge/stages/intake.py +372 -0
  197. spriteforge/stages/preview_assets.py +212 -0
  198. spriteforge/stages/preview_models.py +167 -0
  199. spriteforge/stages/preview_render.py +169 -0
  200. spriteforge/stages/preview_repair.py +61 -0
  201. spriteforge/stages/preview_review.py +169 -0
  202. spriteforge/stages/production.py +170 -0
  203. spriteforge/stages/references.py +105 -0
  204. spriteforge/stages/rewind.py +128 -0
  205. spriteforge/stages/workflow.py +440 -0
  206. spriteforge/templates/phaser/audit.js +45 -0
  207. spriteforge/templates/phaser/font.png +0 -0
  208. spriteforge/templates/phaser/font.xml +1 -0
  209. spriteforge/templates/phaser/preview.html +9 -0
  210. spriteforge/templates/phaser/room.js +49 -0
  211. spriteforge/templates/phaser/vendor/LICENSE.md +20 -0
  212. spriteforge/templates/phaser/vendor/phaser.min.js +1 -0
  213. spriteforge-0.1.1.dist-info/METADATA +364 -0
  214. spriteforge-0.1.1.dist-info/RECORD +218 -0
  215. spriteforge-0.1.1.dist-info/WHEEL +5 -0
  216. spriteforge-0.1.1.dist-info/entry_points.txt +2 -0
  217. spriteforge-0.1.1.dist-info/licenses/LICENSE +27 -0
  218. spriteforge-0.1.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,224 @@
1
+ """Durable one-shot tool confirmations and role-enforced invocation."""
2
+
3
+ import hashlib
4
+ import json
5
+ from uuid import uuid4
6
+
7
+ from pydantic import BaseModel, Field
8
+
9
+ from spriteforge.agents.cancellation import check_active
10
+ from spriteforge.core.security import redact
11
+
12
+ from .client import MCPClient
13
+ from .registry import Registry
14
+
15
+
16
+ class ConfirmationRequired(Exception):
17
+ def __init__(self, ident):
18
+ self.ident = ident
19
+ super().__init__(f"Human confirmation required: {ident}")
20
+
21
+
22
+ class CallIntent(BaseModel):
23
+ id: str
24
+ role: str
25
+ tool: str
26
+ arguments: dict = Field(default_factory=dict)
27
+ fingerprint: str
28
+ prior_status: str
29
+
30
+
31
+ class MCPService:
32
+ def __init__(self, store, client=None):
33
+ self.store = store
34
+ self.registry = Registry(store.path.parent)
35
+ self.client = client or MCPClient()
36
+ with store.connect() as db:
37
+ db.execute("""CREATE TABLE IF NOT EXISTS mcp_calls (
38
+ run_id TEXT, id TEXT, intent TEXT, status TEXT, result TEXT,
39
+ PRIMARY KEY(run_id,id))""")
40
+
41
+ async def discover(self, name):
42
+ server = self.registry.read().mcpServers[name]
43
+ tools = await self.client.discover(server)
44
+ self.registry.discovered(name, server.fingerprint(), tools)
45
+ return tools
46
+
47
+ def pending(self, run_id):
48
+ with self.store.connect() as db:
49
+ return [
50
+ {**json.loads(r["intent"]), "status": r["status"]}
51
+ for r in db.execute(
52
+ "SELECT intent,status FROM mcp_calls WHERE run_id=? AND status='pending'",
53
+ (run_id,),
54
+ )
55
+ ]
56
+
57
+ def row(self, run_id, ident):
58
+ with self.store.connect() as db:
59
+ row = db.execute(
60
+ "SELECT * FROM mcp_calls WHERE run_id=? AND id=?", (run_id, ident)
61
+ ).fetchone()
62
+ return dict(row) if row else None
63
+
64
+ def authorize(self, role, tool):
65
+ allowed = self.registry.allowed(role)
66
+ if tool not in allowed:
67
+ raise PermissionError(f"MCP tool not enabled for {role.name}: {tool}")
68
+ return allowed[tool]
69
+
70
+ async def call(self, run_id, role, tool, arguments, ident=None):
71
+ ident = ident or uuid4().hex
72
+ check_active()
73
+ self.store.append_event(
74
+ run_id, role.name, "mcp.attempted", {"id": ident, "tool": tool, "arguments": arguments}
75
+ )
76
+ try:
77
+ server, definition = self.authorize(role, tool)
78
+ except PermissionError:
79
+ self.store.append_event(
80
+ run_id, role.name, "mcp.denied", {"tool": tool, "arguments": arguments}
81
+ )
82
+ raise
83
+ # Persist only secret-free intent. Credentials are supplied via server env/header bindings.
84
+ if redact(arguments) != arguments:
85
+ raise ValueError("MCP arguments contain secrets; use server environment bindings")
86
+ from jsonschema import Draft202012Validator
87
+ from referencing import Registry as SchemaRegistry
88
+
89
+ Draft202012Validator(definition.get("inputSchema", {}), registry=SchemaRegistry()).validate(
90
+ arguments
91
+ )
92
+ fingerprint = hashlib.sha256(
93
+ (server.fingerprint() + json.dumps(definition, sort_keys=True)).encode()
94
+ ).hexdigest()
95
+ row = self.row(run_id, ident)
96
+ if row:
97
+ intent = CallIntent.model_validate_json(row["intent"])
98
+ if (intent.role, intent.tool, intent.arguments, intent.fingerprint) != (
99
+ role.name,
100
+ tool,
101
+ arguments,
102
+ fingerprint,
103
+ ):
104
+ raise PermissionError("MCP call changed since confirmation")
105
+ if row["status"] == "succeeded":
106
+ return json.loads(row["result"])
107
+ if row["status"] == "denied":
108
+ return {
109
+ "isError": True,
110
+ "content": [{"type": "text", "text": "Human denied this tool call."}],
111
+ }
112
+ if row["status"] == "pending":
113
+ raise ConfirmationRequired(ident)
114
+ raise PermissionError(f"MCP call cannot replay from {row['status']}")
115
+ run = self.store.get_run(run_id)
116
+ if run.status in {"completed", "aborted", "failed"}:
117
+ raise ValueError("Run is not active")
118
+ intent = CallIntent(
119
+ id=ident,
120
+ role=role.name,
121
+ tool=tool,
122
+ arguments=arguments,
123
+ fingerprint=fingerprint,
124
+ prior_status=run.status,
125
+ )
126
+ confirm = server.confirmation.get(definition["name"], True)
127
+ with self.store.connect() as db:
128
+ db.execute(
129
+ "INSERT INTO mcp_calls VALUES(?,?,?,?,NULL)",
130
+ (run_id, ident, intent.model_dump_json(), "pending" if confirm else "approved"),
131
+ )
132
+ self.store._event(db, run_id, role.name, "mcp.requested", intent.model_dump())
133
+ if confirm:
134
+ db.execute("UPDATE runs SET status='paused' WHERE id=?", (run_id,))
135
+ if confirm:
136
+ raise ConfirmationRequired(ident)
137
+ return await self.execute(run_id, ident, role)
138
+
139
+ async def respond(self, run_id, ident, approve, role):
140
+ with self.store.connect() as db:
141
+ changed = db.execute(
142
+ "UPDATE mcp_calls SET status=? WHERE run_id=? AND id=? AND status='pending'",
143
+ ("approved" if approve else "denied", run_id, ident),
144
+ )
145
+ if not changed.rowcount:
146
+ raise ValueError("Confirmation already answered or missing")
147
+ self.store._event(
148
+ db, run_id, "Human", "mcp.confirmed" if approve else "mcp.rejected", {"id": ident}
149
+ )
150
+ if not approve:
151
+ self.restore(run_id, ident)
152
+ return {"denied": True}
153
+ try:
154
+ return await self.execute(run_id, ident, role)
155
+ finally:
156
+ self.restore(run_id, ident)
157
+
158
+ def restore(self, run_id, ident):
159
+ intent = CallIntent.model_validate_json(self.row(run_id, ident)["intent"])
160
+ if not self.pending(run_id):
161
+ with self.store.connect() as db:
162
+ db.execute(
163
+ "UPDATE runs SET status=? WHERE id=? AND status='paused'",
164
+ (intent.prior_status, run_id),
165
+ )
166
+
167
+ async def execute(self, run_id, ident, role):
168
+ if self.store.get_run(run_id).status in {"completed", "aborted", "failed"}:
169
+ raise PermissionError("Run is no longer active")
170
+ intent = CallIntent.model_validate_json(self.row(run_id, ident)["intent"])
171
+ server, definition = self.authorize(role, intent.tool)
172
+ fingerprint = hashlib.sha256(
173
+ (server.fingerprint() + json.dumps(definition, sort_keys=True)).encode()
174
+ ).hexdigest()
175
+ if intent.role != role.name or intent.fingerprint != fingerprint:
176
+ raise PermissionError("Server, role or tool changed after confirmation")
177
+ check_active()
178
+ with self.store.connect() as db:
179
+ changed = db.execute(
180
+ "UPDATE mcp_calls SET status='running' WHERE run_id=? AND id=? AND status='approved'",
181
+ (run_id, ident),
182
+ )
183
+ if not changed.rowcount:
184
+ raise PermissionError("Confirmation already consumed")
185
+ self.store._event(
186
+ db,
187
+ run_id,
188
+ role.name,
189
+ "mcp.called",
190
+ {"id": ident, "tool": intent.tool, "arguments": intent.arguments},
191
+ )
192
+ try:
193
+ from spriteforge.config import BudgetConfig
194
+ from spriteforge.core.cost import Budget
195
+
196
+ config = BudgetConfig.model_validate(
197
+ self.store.get_run(run_id).config.get("budget", {})
198
+ )
199
+ with Budget(self.store, run_id, config).attempt("mcp", intent.tool.split(".", 1)[0]):
200
+ result = redact(
201
+ await self.client.call(server, definition["name"], intent.arguments)
202
+ )
203
+ except BaseException as exc:
204
+ with self.store.connect() as db:
205
+ db.execute(
206
+ "UPDATE mcp_calls SET status='failed' WHERE run_id=? AND id=?", (run_id, ident)
207
+ )
208
+ self.store._event(
209
+ db, run_id, role.name, "mcp.failed", {"id": ident, "error": type(exc).__name__}
210
+ )
211
+ raise
212
+ with self.store.connect() as db:
213
+ db.execute(
214
+ "UPDATE mcp_calls SET status='succeeded',result=? WHERE run_id=? AND id=?",
215
+ (json.dumps(result), run_id, ident),
216
+ )
217
+ self.store._event(
218
+ db,
219
+ run_id,
220
+ role.name,
221
+ "mcp.completed",
222
+ {"id": ident, "tool": intent.tool, "isError": result.get("isError", False)},
223
+ )
224
+ return result
@@ -0,0 +1,6 @@
1
+ {
2
+ "transport": "stdio",
3
+ "command": "npx",
4
+ "args": ["-y", "@upstash/context7-mcp"],
5
+ "enabled_roles": {}
6
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "transport": "stdio",
3
+ "command": "uvx",
4
+ "args": ["mcp-server-fetch", "--no-readability-js"],
5
+ "enabled_roles": {}
6
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "transport": "stdio",
3
+ "command": "python",
4
+ "args": ["-m", "spriteforge.mcp.filesystem_read", "workspace/runs"],
5
+ "enabled_roles": {}
6
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "transport": "stdio",
3
+ "command": "python",
4
+ "args": ["-m", "spriteforge.mcp.pixel_tools", "workspace"],
5
+ "enabled_roles": {}
6
+ }
@@ -0,0 +1,10 @@
1
+ # Memory safety boundary
2
+
3
+ Secret detection uses heuristics, not guarantees. Known environment credentials,
4
+ bearer tokens, private-key headers, and credential-style assignments are rejected;
5
+ arbitrary or unlabeled secrets may pass these checks.
6
+
7
+ Redaction happens at the source before the Librarian sees run data. Producers must
8
+ redact run events, tool results, and artifacts before including them in a Librarian
9
+ request. The memory writer's detection is a second check, not a replacement for
10
+ source redaction. Never submit API keys or secrets as knowledge.
File without changes
@@ -0,0 +1,131 @@
1
+ from pathlib import Path
2
+
3
+ import typer
4
+
5
+ from spriteforge.core.credentials import load_credentials
6
+
7
+ from .okf.seed import seed
8
+ from .okf.store import KnowledgeStore
9
+ from .okf.validate import validate as validate_corpus
10
+
11
+ app = typer.Typer(no_args_is_help=True, help="Local OKF knowledge and retrieval.")
12
+
13
+
14
+ def open_store(workspace: Path) -> KnowledgeStore:
15
+ load_credentials()
16
+ store = KnowledgeStore(workspace / "memory")
17
+ if not store.scan()[1]:
18
+ seed(store)
19
+ return store
20
+
21
+
22
+ @app.command("sync-catalog")
23
+ def sync_catalog_command(workspace: Path = Path("workspace")):
24
+ """Mirror asset packs, tile sets and actors into memory under catalog/ (one commit)."""
25
+ from .okf.catalog import sync_catalog
26
+
27
+ result = sync_catalog(open_store(workspace))
28
+ typer.echo(
29
+ f"catalog: {result['total']} entries, {result['updated']} written, "
30
+ f"{result['deprecated']} deprecated"
31
+ )
32
+
33
+
34
+ @app.command()
35
+ def init(workspace: Path = Path("workspace")):
36
+ """Initialize the local Git corpus and seed library without overwriting entries."""
37
+ store = open_store(workspace)
38
+ typer.echo(f"Knowledge corpus: {store.root}")
39
+
40
+
41
+ @app.command()
42
+ def search(
43
+ query: str,
44
+ workspace: Path = Path("workspace"),
45
+ scope: list[str] | None = None,
46
+ token_budget: int = 1024,
47
+ min_score: float | None = None,
48
+ relative_score: float | None = None,
49
+ verbose: bool = False,
50
+ ):
51
+ """Return ranked snippets with trust tiers and path-derived IDs."""
52
+ store = open_store(workspace)
53
+ for snippet in store.retrieve(
54
+ query, scope, token_budget, min_score=min_score, relative_score=relative_score
55
+ ):
56
+ flags = "; bootstrap, unreviewed" if snippet.bootstrap_unreviewed else ""
57
+ flags += " STALE" if snippet.stale else ""
58
+ if snippet.status != "stable":
59
+ flags += " " + snippet.status.upper()
60
+ if verbose:
61
+ flags += f" score={snippet.score:.3f}"
62
+ typer.echo(f"[{snippet.trust_tier}{flags}] {snippet.id}: {snippet.title}\n{snippet.text}\n")
63
+
64
+
65
+ @app.command("validate")
66
+ def validate_command(workspace: Path = Path("workspace")):
67
+ """Validate local OKF and run the optional external `okf validate` tool."""
68
+ report = validate_corpus(open_store(workspace))
69
+ for issue in report.errors:
70
+ typer.echo(f"ERROR: {issue}")
71
+ for issue in report.warnings:
72
+ typer.echo(f"WARNING: {issue}")
73
+ typer.echo(f"External okf: {report.external}")
74
+ typer.echo("PASS" if not report.errors else "FAIL")
75
+ if report.errors:
76
+ raise typer.Exit(1)
77
+
78
+
79
+ @app.command()
80
+ def index(directory: str = "", workspace: Path = Path("workspace"), scope: list[str] | None = None):
81
+ """List summaries before selecting a full entry."""
82
+ typer.echo(open_store(workspace).index(directory, scope))
83
+
84
+
85
+ @app.command()
86
+ def show(id: str, workspace: Path = Path("workspace"), scope: list[str] | None = None):
87
+ """Load one full concept after search/index discovery."""
88
+ from .okf.codec import serialize
89
+
90
+ typer.echo(serialize(open_store(workspace).show(id, scope)))
91
+
92
+
93
+ @app.command()
94
+ def promote(id: str, by: str, workspace: Path = Path("workspace")):
95
+ """Explicit human sign-off; agent edits subsequently clear this verification."""
96
+ open_store(workspace).promote(id, by if by.startswith("human:") else "human:" + by)
97
+ typer.echo(f"Human verification recorded for {id}")
98
+
99
+
100
+ @app.command()
101
+ def export(destination: Path, workspace: Path = Path("workspace")):
102
+ """Export public concepts and fresh indexes, excluding private data and Git history."""
103
+ open_store(workspace).export_public(destination)
104
+ typer.echo(f"Public knowledge exported to {destination}")
105
+
106
+
107
+ @app.command()
108
+ def mcp(workspace: Path = Path("workspace"), allow_private: bool = False):
109
+ """Expose read-only corpus tools over MCP stdio; stdout is protocol-only."""
110
+ from spriteforge.mcp.memory import create_server
111
+
112
+ create_server(open_store(workspace), allow_private=allow_private).run(transport="stdio")
113
+
114
+
115
+ @app.command()
116
+ def review(
117
+ id: str | None = typer.Argument(None),
118
+ confirm: bool = False,
119
+ workspace: Path = Path("workspace"),
120
+ ):
121
+ """List bootstrap entries awaiting review, or explicitly confirm one."""
122
+ store = open_store(workspace)
123
+ if id:
124
+ if not confirm:
125
+ raise typer.BadParameter("Review requires --confirm")
126
+ store.review(id)
127
+ typer.echo(f"Review recorded for {id}")
128
+ else:
129
+ for entry in store.scan()[0]:
130
+ if entry.metadata.model_extra.get("x-spriteforge-review") == "unreviewed":
131
+ typer.echo(f"{entry.id}: {entry.title} [bootstrap, unreviewed]")
@@ -0,0 +1 @@
1
+ """Google OKF v0.2 Markdown knowledge with local search and auditable writes."""