niahere 0.3.8 → 0.3.10

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "niahere",
3
- "version": "0.3.8",
3
+ "version": "0.3.10",
4
4
  "description": "A personal AI assistant daemon — chat, scheduled jobs, persona system, extensible via skills.",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -105,13 +105,15 @@ def encode_file(path: str) -> str:
105
105
  return base64.b64encode(f.read()).decode("utf-8")
106
106
 
107
107
 
108
- def resolve_output_path(output: str | None, ext: str = ".png") -> Path:
108
+ def resolve_output_path(output: str | None, ext: str = ".png", index: int | None = None) -> Path:
109
+ # For multi-image output, append _1, _2, ... before the extension.
110
+ suffix = f"_{index + 1}" if index is not None else ""
109
111
  if output:
110
112
  out = Path(output).expanduser()
111
113
  if out.suffix:
112
- return out
113
- return out / f"image_{time.strftime(TIMESTAMP_FORMAT)}{ext}"
114
- return Path(f"/tmp/image_{time.strftime(TIMESTAMP_FORMAT)}{ext}")
114
+ return out.with_name(f"{out.stem}{suffix}{out.suffix}")
115
+ return out / f"image_{time.strftime(TIMESTAMP_FORMAT)}{suffix}{ext}"
116
+ return Path(f"/tmp/image_{time.strftime(TIMESTAMP_FORMAT)}{suffix}{ext}")
115
117
 
116
118
 
117
119
  def read_config_key(key: str) -> str:
@@ -159,14 +161,14 @@ def generate_openai(
159
161
  quality: str,
160
162
  reference_path: str | None = None,
161
163
  n: int = 1,
162
- ) -> tuple[bytes, str]:
163
- """Generate image via OpenAI Images API."""
164
+ ) -> list[tuple[bytes, str]]:
165
+ """Generate image(s) via OpenAI Images API. Returns one entry per image."""
164
166
  if reference_path and Path(reference_path).is_file():
165
167
  return _openai_edit(api_key, prompt, reference_path, model, size, quality, n)
166
168
  return _openai_generate(api_key, prompt, model, size, quality, n)
167
169
 
168
170
 
169
- def _openai_generate(api_key: str, prompt: str, model: str, size: str, quality: str, n: int) -> tuple[bytes, str]:
171
+ def _openai_generate(api_key: str, prompt: str, model: str, size: str, quality: str, n: int) -> list[tuple[bytes, str]]:
170
172
  url = "https://api.openai.com/v1/images/generations"
171
173
  payload: dict = {
172
174
  "model": model,
@@ -191,7 +193,7 @@ def _openai_generate(api_key: str, prompt: str, model: str, size: str, quality:
191
193
 
192
194
  def _openai_edit(
193
195
  api_key: str, prompt: str, reference_path: str, model: str, size: str, quality: str, n: int
194
- ) -> tuple[bytes, str]:
196
+ ) -> list[tuple[bytes, str]]:
195
197
  """Use OpenAI images/edits endpoint with a reference image."""
196
198
  import io
197
199
 
@@ -235,7 +237,7 @@ def _openai_edit(
235
237
  return _openai_request(req)
236
238
 
237
239
 
238
- def _openai_request(req: urllib.request.Request) -> tuple[bytes, str]:
240
+ def _openai_request(req: urllib.request.Request) -> list[tuple[bytes, str]]:
239
241
  try:
240
242
  with urllib.request.urlopen(req, timeout=180) as resp:
241
243
  response = json.loads(resp.read().decode("utf-8"))
@@ -247,11 +249,11 @@ def _openai_request(req: urllib.request.Request) -> tuple[bytes, str]:
247
249
  if not data_list:
248
250
  raise RuntimeError(f"No data in OpenAI response: {json.dumps(response, indent=2)}")
249
251
 
250
- b64 = data_list[0].get("b64_json")
251
- if not b64:
252
+ images = [(base64.b64decode(item["b64_json"]), "image/png") for item in data_list if item.get("b64_json")]
253
+ if not images:
252
254
  raise RuntimeError("No b64_json in OpenAI response.")
253
255
 
254
- return base64.b64decode(b64), "image/png"
256
+ return images
255
257
 
256
258
 
257
259
  # --- Gemini Generation ---
@@ -422,7 +424,7 @@ Examples:
422
424
  raise SystemExit(f"2K is only supported on gpt-image-2 (got --model {model}).")
423
425
  size_map = OPENAI_SIZE_MAP_2K if args.resolution == "2K" else OPENAI_SIZE_MAP_1K
424
426
  size = size_map.get(args.aspect_ratio, size_map["1:1"])
425
- image_data, mime = generate_openai(
427
+ images = generate_openai(
426
428
  api_key=api_key,
427
429
  prompt=args.prompt,
428
430
  model=model,
@@ -432,21 +434,27 @@ Examples:
432
434
  n=args.n,
433
435
  )
434
436
  else:
435
- image_data, mime = generate_gemini(
436
- api_key=api_key,
437
- prompt=args.prompt,
438
- model=model,
439
- aspect_ratio=args.aspect_ratio,
440
- resolution=args.resolution,
441
- reference_path=ref,
442
- )
443
-
444
- ext = ".png" if "png" in mime else ".jpg"
445
- out = resolve_output_path(args.output, ext)
446
- out.parent.mkdir(parents=True, exist_ok=True)
447
- out.write_bytes(image_data)
448
- print(f"Saved: {out}")
449
- print(f"Provider: {provider} | Model: {model} | Ratio: {args.aspect_ratio} | Resolution: {args.resolution}")
437
+ images = [
438
+ generate_gemini(
439
+ api_key=api_key,
440
+ prompt=args.prompt,
441
+ model=model,
442
+ aspect_ratio=args.aspect_ratio,
443
+ resolution=args.resolution,
444
+ reference_path=ref,
445
+ )
446
+ ]
447
+
448
+ multiple = len(images) > 1
449
+ for idx, (image_data, mime) in enumerate(images):
450
+ ext = ".png" if "png" in mime else ".jpg"
451
+ out = resolve_output_path(args.output, ext, index=idx if multiple else None)
452
+ out.parent.mkdir(parents=True, exist_ok=True)
453
+ out.write_bytes(image_data)
454
+ print(f"Saved: {out}")
455
+ print(
456
+ f"Provider: {provider} | Model: {model} | Ratio: {args.aspect_ratio} | Resolution: {args.resolution} | Images: {len(images)}"
457
+ )
450
458
  except Exception as exc:
451
459
  print(f"Error: {exc}", file=sys.stderr)
452
460
  raise SystemExit(1) from exc
@@ -136,6 +136,7 @@ class SlackChannel implements Channel {
136
136
  });
137
137
 
138
138
  let botUserId: string | undefined;
139
+ let botId: string | undefined;
139
140
 
140
141
  const watchReloader = new SlackWatchReloader();
141
142
  const attachmentCache = new SlackAttachmentCache(botToken);
@@ -237,7 +238,7 @@ class SlackChannel implements Channel {
237
238
  inclusive: true,
238
239
  });
239
240
  const parentMsg = parent.messages?.[0];
240
- if (parentMsg && (parentMsg.user === botUserId || parentMsg.bot_id)) {
241
+ if (parentMsg && (parentMsg.user === botUserId || (botId && parentMsg.bot_id === botId))) {
241
242
  isActiveThread = true;
242
243
  log.debug(
243
244
  { channel: msg.channel, thread_ts: msg.thread_ts },
@@ -493,7 +494,8 @@ class SlackChannel implements Channel {
493
494
  try {
494
495
  const auth = await app.client.auth.test();
495
496
  botUserId = auth.user_id as string | undefined;
496
- log.info({ botUserId }, "slack bot authenticated");
497
+ botId = auth.bot_id as string | undefined;
498
+ log.info({ botUserId, botId }, "slack bot authenticated");
497
499
  } catch (err) {
498
500
  log.warn({ err }, "could not get slack bot user ID");
499
501
  }