gemcode 0.3.97__py3-none-any.whl → 0.3.99__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.
gemcode/invoke.py CHANGED
@@ -184,15 +184,33 @@ async def run_turn(
184
184
  # user-granted "Files and Folders" permissions on first access.
185
185
  # If approved once, we don't re-prompt for the rest of this session.
186
186
  attach_allow = True
187
- if cfg is not None:
188
- attach_allow = bool(getattr(cfg, "interactive_permission_ask", False))
189
- attach_allow = attach_allow and hasattr(sys.stdin, "isatty") and sys.stdin.isatty()
190
- if attach_allow and not bool(getattr(cfg, "_attachments_allowed", False)):
191
- attach_allow = _prompt_yes_no(
192
- "Allow GemCode to read and upload the attached file(s) from disk? (y/n) "
193
- )
194
- if attach_allow:
187
+ if hasattr(sys.stdin, "isatty") and sys.stdin.isatty():
188
+ # Default-on: attachments can read any local file path (not workspace-scoped),
189
+ # but we ask once per session so the user is in control and macOS can trigger
190
+ # its permission prompt at the moment we attempt the read.
191
+ attach_allow = os.environ.get("GEMCODE_ATTACHMENTS_ASK", "1").lower() not in (
192
+ "0",
193
+ "false",
194
+ "no",
195
+ "off",
196
+ )
197
+ if cfg is not None:
198
+ # If user already approved earlier in this session, don't prompt again.
199
+ if bool(getattr(cfg, "_attachments_allowed", False)):
200
+ attach_allow = True
201
+ # If yes-to-all is enabled, auto-allow attachments.
202
+ elif bool(getattr(cfg, "yes_to_all", False)):
203
+ attach_allow = True
195
204
  object.__setattr__(cfg, "_attachments_allowed", True)
205
+ elif attach_allow:
206
+ attach_allow = _prompt_yes_no(
207
+ "Allow GemCode to read and upload the attached file(s) from disk? (y/n) "
208
+ )
209
+ if attach_allow:
210
+ object.__setattr__(cfg, "_attachments_allowed", True)
211
+ else:
212
+ # Non-interactive sessions can't prompt; default to allow.
213
+ attach_allow = True
196
214
  effective_attachments = attachment_paths if attach_allow else None
197
215
 
198
216
  root = cfg.project_root if cfg is not None else Path.cwd()
@@ -97,6 +97,18 @@ def _infer_mime(path: Path, data: bytes) -> tuple[str, list[str]]:
97
97
  return "application/octet-stream", warnings
98
98
 
99
99
 
100
+ def _is_supported_mime(m: str) -> bool:
101
+ mm = (m or "").strip().lower()
102
+ if not mm:
103
+ return False
104
+ if mm.startswith(("image/", "audio/", "video/", "text/")):
105
+ return True
106
+ if mm == "application/pdf":
107
+ return True
108
+ # Most other application/* types are rejected by Gemini file parts.
109
+ return False
110
+
111
+
100
112
  def build_user_content(
101
113
  prompt: str,
102
114
  attachment_paths: Sequence[Path | str] | None,
@@ -135,6 +147,12 @@ def build_user_content(
135
147
  continue
136
148
  mime, mw = _infer_mime(p, data)
137
149
  warnings.extend(mw)
150
+ if not _is_supported_mime(mime) or mime == "application/octet-stream":
151
+ warnings.append(
152
+ f"unsupported attachment type for {p} (mime={mime}); "
153
+ "skipping (export to PDF/image/text if needed)"
154
+ )
155
+ continue
138
156
  parts.append(types.Part(inline_data=types.Blob(data=data, mime_type=mime)))
139
157
 
140
158
  text = (prompt or "").strip() or (
@@ -100,16 +100,51 @@ def _build_prompt(transcript_lines: list[str], *, focus: str = "") -> str:
100
100
  )
101
101
 
102
102
 
103
- def _call_summary_model(*, model: str, prompt: str) -> dict[str, Any]:
103
+ def _extract_json_object(text: str) -> dict[str, Any] | None:
104
+ """
105
+ Best-effort parse:
106
+ - strict JSON object
107
+ - JSON object embedded in extra text (common model behavior)
108
+ """
109
+ raw = (text or "").strip()
110
+ if not raw:
111
+ return None
112
+ try:
113
+ obj = json.loads(raw)
114
+ return obj if isinstance(obj, dict) else None
115
+ except Exception:
116
+ pass
117
+
118
+ # Try to recover embedded JSON: find the largest {...} span.
119
+ start = raw.find("{")
120
+ end = raw.rfind("}")
121
+ if start == -1 or end == -1 or end <= start:
122
+ return None
123
+ candidate = raw[start : end + 1].strip()
124
+ try:
125
+ obj = json.loads(candidate)
126
+ return obj if isinstance(obj, dict) else None
127
+ except Exception:
128
+ return None
129
+
130
+
131
+ def _call_summary_model(*, model: str, prompt: str) -> tuple[dict[str, Any] | None, str]:
104
132
  api_key = os.environ.get("GOOGLE_API_KEY")
105
133
  if not api_key:
106
134
  raise RuntimeError("GOOGLE_API_KEY not set")
107
135
 
108
136
  client = Client(api_key=api_key)
137
+ cfg = types.GenerateContentConfig(temperature=0.2)
138
+ # Prefer structured JSON output when supported by the SDK/model.
139
+ try:
140
+ setattr(cfg, "response_mime_type", "application/json")
141
+ except Exception:
142
+ pass
143
+
109
144
  resp = client.models.generate_content(
110
- model=model,
111
- contents=[types.Content(role="user", parts=[types.Part(text=prompt)])],
112
- config=types.GenerateContentConfig(temperature=0.2),
145
+ model=model,
146
+ contents=[types.Content(role="user", parts=[types.Part(text=prompt)])],
147
+ config=cfg,
113
148
  )
114
149
 
115
150
  out_parts: list[str] = []
@@ -127,14 +162,7 @@ def _call_summary_model(*, model: str, prompt: str) -> dict[str, Any]:
127
162
  text = "".join(out_parts).strip()
128
163
  if not text:
129
164
  raise RuntimeError("session summariser returned empty text")
130
-
131
- try:
132
- data = json.loads(text)
133
- except Exception as e:
134
- raise RuntimeError(f"session summariser returned invalid JSON: {e}") from e
135
- if not isinstance(data, dict):
136
- raise RuntimeError("session summariser returned non-object JSON")
137
- return data
165
+ return _extract_json_object(text), text
138
166
 
139
167
 
140
168
  def summarise_session(
@@ -149,7 +177,18 @@ def summarise_session(
149
177
  return {"error": "session transcript is empty", "session_id": session_id}
150
178
 
151
179
  prompt = _build_prompt(transcript_lines, focus=focus)
152
- data = _call_summary_model(model=model, prompt=prompt)
180
+ data, raw_text = _call_summary_model(model=model, prompt=prompt)
181
+ if data is None:
182
+ # Graceful fallback: save raw text as summary_markdown so /summarise never hard-fails
183
+ # due to JSON formatting drift.
184
+ data = {
185
+ "title": f"Session {session_id[:8]}",
186
+ "summary_markdown": raw_text[:20_000],
187
+ "memory_facts": [],
188
+ "user_facts": [],
189
+ "notes_markdown": "",
190
+ "open_items": [],
191
+ }
153
192
 
154
193
  title = str(data.get("title") or f"Session {session_id[:8]}").strip()[:120]
155
194
  summary_markdown = str(data.get("summary_markdown") or "").strip()
@@ -219,6 +258,7 @@ def summarise_session(
219
258
  "summary_path": str(out_path),
220
259
  "title": title,
221
260
  "summary_markdown": summary_markdown,
261
+ "used_json": bool(data is not None and _extract_json_object(raw_text) is not None),
222
262
  "memory_facts_saved": saved_memory,
223
263
  "user_facts_saved": saved_user,
224
264
  "notes_status": note_status,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gemcode
3
- Version: 0.3.97
3
+ Version: 0.3.99
4
4
  Summary: Local-first coding agent on Google Gemini + ADK
5
5
  Author: GemCode Contributors
6
6
  License: Apache License
@@ -21,7 +21,7 @@ gemcode/ide_protocol.py,sha256=WJO4KdwyxjQcH1O_vTn7SPuy1ZZMm0eC8_xRLA9RYQo,2108
21
21
  gemcode/ide_stdio.py,sha256=qDZ8qCR0kWipvyxLJ3tbZfAXChZtosi46dLeNuMejFk,11066
22
22
  gemcode/intent_classifier.py,sha256=YfRVEe8gHeKlRkjuSWef1bZ0MPBwyYMp5jymP5Vig5U,8507
23
23
  gemcode/interactions.py,sha256=B0b3QNE_I2i5_HtiebX4ehhjlc4Nbqjf_XbvcTLyJT0,641
24
- gemcode/invoke.py,sha256=mjCxQGlkZVaIh2eRxsxaojZGrV3FikMqoVCpNo0CV50,11904
24
+ gemcode/invoke.py,sha256=CpDnz3v8NdWPV9JTJVfh2nzkIGN7I7ihIw452QJhEcI,12694
25
25
  gemcode/kaira_daemon.py,sha256=Bzkpc96HocfYAV9D5skid_Gi4bJDOLgO5YlD8vbTgyY,6960
26
26
  gemcode/learning.py,sha256=o4Ivczm626NPRiNbSEb7-RvKJMefnv0ZpYt4UB2C3JA,3856
27
27
  gemcode/limits.py,sha256=3j6N8V643X7-nP-cAIf37Xg9bkGpQlEJB3nPptApQWk,2504
@@ -31,7 +31,7 @@ gemcode/mcp_loader.py,sha256=alipHTl5aA7ZCPG6Rq9cyy3UZLsdCra0CETb_fRJl_k,4964
31
31
  gemcode/modality_tools.py,sha256=L2U756l0YM4SOUx5YxcKEkjSF1QOGzFOSE4o3U0q-4Y,7213
32
32
  gemcode/model_errors.py,sha256=j1nb1dopJyZ6MQQvuuADBqvmcqdL80kQWACuWKMkP4Q,4185
33
33
  gemcode/model_routing.py,sha256=_8mnXNwxMPA8wAfl-Yx5lWNgjhyWYTCCCMGlqdGAw90,5174
34
- gemcode/multimodal_input.py,sha256=FrfwcmqgDKnPDWxj76GOltl34D3PNQHxgbSR7ykL8SU,4450
34
+ gemcode/multimodal_input.py,sha256=eLFNj_yAykvoTMyiv0hGUXMgb6xlN4pyZ6X--swhnw0,5015
35
35
  gemcode/openapi_loader.py,sha256=g_NZD8YL9_9iIJJ9qykhdbBrylJ1195A4FyHGC0mroc,4157
36
36
  gemcode/output_styles.py,sha256=6ZgbEsEM5qbdGDZMuRFRHAmHCoKpi53SqPqXZ2sfbS4,2189
37
37
  gemcode/paths.py,sha256=UQp4R4sUBv7HsM2OVoGlPxyOIOQZE5wpY53G6nHpB5Q,2671
@@ -47,7 +47,7 @@ gemcode/review_agent.py,sha256=4t7_5-aE60b4-EheJ_eSB_H2eQYf9GppKoui6jw0TME,5264
47
47
  gemcode/rules.py,sha256=Itg02VpifOo6jqGj5xwna_ahaPPb0OVtaeR2cNI0pLE,3018
48
48
  gemcode/session_runtime.py,sha256=MGOWWUz4ZUnWkuaYkc5EZ_uYYIvLjJc1N35X2GUX79k,23489
49
49
  gemcode/session_store.py,sha256=POUT_QQf715c74jbXj0s5vCd4dlAgJz_CLsIWuEUoO0,6051
50
- gemcode/session_summariser.py,sha256=ZXBD9vgvZQ7gueFyckKcIOirq_15uVjsZqjHDa3yE-o,7237
50
+ gemcode/session_summariser.py,sha256=ixRJVGbgWGWP-tT8hGLEqBlZ3uMXJlwQdbFsYfwF4hk,8370
51
51
  gemcode/skills.py,sha256=nnrzYUCiuEkU_i57p_jJpPHRfw1t2t2EA3pJHNqvpzw,12554
52
52
  gemcode/slash_commands.py,sha256=bcD-S_H7p7AlTli6g2dLPPG46HejPje0Imb3ScDTCaQ,798
53
53
  gemcode/thinking.py,sha256=-1TVkOMG-7CSQN0Mc18EqINkUxWOMBgeTlF7CX9zYL4,4641
@@ -107,9 +107,9 @@ gemcode/web/__init__.py,sha256=EysmUAWs6g-lmMk4VFljKfaHVrEgb_FiIzwQmBdORJc,40
107
107
  gemcode/web/sse_adapter.py,sha256=fXhKxn_bdJJUGqlmvkxLNSYL-ZiIZDaLHtQCF_BheRc,7108
108
108
  gemcode/web/terminal_repl.py,sha256=fQt895g0qcr6VBhXfv_5b_bsC5zHT5-MO0ysBdgi2Fg,3886
109
109
  gemcode/web/web_sse_compat.py,sha256=9A2s-GI7El7AotJqhO263FrLwppCXXkdydZ5EiOQbao,504
110
- gemcode-0.3.97.dist-info/licenses/LICENSE,sha256=TD4524qn-W8Z07GTDnag-9jJPFutFZNB0a1WbMHPC54,8388
111
- gemcode-0.3.97.dist-info/METADATA,sha256=-rLJq9S6zklxs9L4oMQ3RVatlE4MkOUlcfvtkgTZvwo,17084
112
- gemcode-0.3.97.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
113
- gemcode-0.3.97.dist-info/entry_points.txt,sha256=cZdLTLDiHbks7OSUCuxCh66dCWeQdpLR8BozoqfEjV4,45
114
- gemcode-0.3.97.dist-info/top_level.txt,sha256=UYrjULLBY2bcgK6KI6flomJWmsbDXu7n0rvW2SWFrbo,8
115
- gemcode-0.3.97.dist-info/RECORD,,
110
+ gemcode-0.3.99.dist-info/licenses/LICENSE,sha256=TD4524qn-W8Z07GTDnag-9jJPFutFZNB0a1WbMHPC54,8388
111
+ gemcode-0.3.99.dist-info/METADATA,sha256=xCMSBtFzQrLEDtt9BawkHs1NfdmLTQbGf0lcJ-RHsWg,17084
112
+ gemcode-0.3.99.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
113
+ gemcode-0.3.99.dist-info/entry_points.txt,sha256=cZdLTLDiHbks7OSUCuxCh66dCWeQdpLR8BozoqfEjV4,45
114
+ gemcode-0.3.99.dist-info/top_level.txt,sha256=UYrjULLBY2bcgK6KI6flomJWmsbDXu7n0rvW2SWFrbo,8
115
+ gemcode-0.3.99.dist-info/RECORD,,