@meridiona/meridian-darwin-arm64 1.23.11 → 1.24.1
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/VERSION +1 -1
- package/bin/meridian +0 -0
- package/package.json +1 -1
- package/scripts/install-from-bundle.sh +9 -0
- package/scripts/meridian-cli.sh +169 -4
- package/services/agents/server.py +71 -2
- package/services/pyproject.toml +1 -1
- package/ui.tar.gz +0 -0
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
1.
|
|
1
|
+
1.24.1
|
package/bin/meridian
CHANGED
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@meridiona/meridian-darwin-arm64",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.24.1",
|
|
4
4
|
"description": "Prebuilt Meridian app for macOS arm64 (daemon binary + dashboard + Python services). Installed via @meridiona/meridian.",
|
|
5
5
|
"homepage": "https://github.com/Meridiona/meridian",
|
|
6
6
|
"repository": {
|
|
@@ -563,6 +563,15 @@ _final_ui_hash="${_new_ui_hash:-${_OLD_UI_HASH}}"
|
|
|
563
563
|
|
|
564
564
|
ok "all daemons installed"
|
|
565
565
|
|
|
566
|
+
# Pipeline smoke test — verify both LLM stages return valid output (no DB writes).
|
|
567
|
+
echo ""
|
|
568
|
+
info "Running pipeline smoke test (this exercises the model — may take ~30s)…"
|
|
569
|
+
if bash "${APP_ROOT}/scripts/meridian-cli.sh" smoke; then
|
|
570
|
+
ok "pipeline smoke passed — classification and worklog synthesis are working"
|
|
571
|
+
else
|
|
572
|
+
warn "pipeline smoke found issues — run 'meridian doctor' for remedies"
|
|
573
|
+
fi
|
|
574
|
+
|
|
566
575
|
echo ""
|
|
567
576
|
echo "✓ Meridian installed at ${APP_ROOT}"
|
|
568
577
|
echo " meridian status # check the daemons"
|
package/scripts/meridian-cli.sh
CHANGED
|
@@ -43,7 +43,8 @@ Commands:
|
|
|
43
43
|
target: daemon|daemon-error|screenpipe|screenpipe-error|ui|ui-error|mlx-server|mlx-server-error
|
|
44
44
|
-f Follow (stream)
|
|
45
45
|
-n N Last N lines (default 100)
|
|
46
|
-
doctor Run environment health checks
|
|
46
|
+
doctor Run environment health checks (includes pipeline smoke)
|
|
47
|
+
smoke Dry-run both LLM pipeline stages — no DB writes
|
|
47
48
|
worklog-status Show today's PM worklogs (done/pending/drafted/posted + comments)
|
|
48
49
|
[--day YYYY-MM-DD]
|
|
49
50
|
config edit Open the repo-root .env in $EDITOR
|
|
@@ -227,26 +228,34 @@ _daemon_bin() {
|
|
|
227
228
|
}
|
|
228
229
|
|
|
229
230
|
cmd_doctor() {
|
|
230
|
-
local bin
|
|
231
|
+
local bin rc=0
|
|
231
232
|
if bin="$(_daemon_bin)"; then
|
|
232
233
|
set +e
|
|
233
234
|
if [[ "$*" == *--fix* ]]; then
|
|
234
235
|
# --fix has interactive guided prompts — the user is present, so run
|
|
235
236
|
# without the alarm (which would kill a prompt waiting for input).
|
|
236
237
|
"$bin" doctor "$@"
|
|
238
|
+
rc=$?
|
|
237
239
|
else
|
|
238
240
|
# Guard with a perl alarm so a stale binary (one that predates
|
|
239
241
|
# `doctor` and would fall through to starting the daemon) can never
|
|
240
242
|
# hang the terminal. The Rust report colourises itself on a tty.
|
|
241
243
|
perl -e 'alarm shift @ARGV; exec @ARGV' 30 "$bin" doctor "$@"
|
|
244
|
+
rc=$?
|
|
242
245
|
fi
|
|
243
|
-
local rc=$?
|
|
244
246
|
set -e
|
|
245
247
|
# 0 = healthy, 1 = critical issues found — both are real doctor runs.
|
|
246
|
-
if [[ $rc -eq 0 || $rc -eq 1 ]]; then
|
|
248
|
+
if [[ $rc -eq 0 || $rc -eq 1 ]]; then
|
|
249
|
+
# Append classification smoke (fast path, ~30s max). Failures are
|
|
250
|
+
# informational — they don't override the doctor exit code, since the
|
|
251
|
+
# doctor already surfaces the MLX health state.
|
|
252
|
+
cmd_smoke --classify-only || true
|
|
253
|
+
return $rc
|
|
254
|
+
fi
|
|
247
255
|
warn "health engine timed out or is stale — rebuild: cargo build --release"
|
|
248
256
|
fi
|
|
249
257
|
_doctor_fallback
|
|
258
|
+
cmd_smoke --classify-only || true
|
|
250
259
|
}
|
|
251
260
|
|
|
252
261
|
# Minimal bash-only checks for when the daemon binary is unavailable.
|
|
@@ -270,6 +279,161 @@ _doctor_fallback() {
|
|
|
270
279
|
[[ $DOCTOR_FAILURES -eq 0 ]]
|
|
271
280
|
}
|
|
272
281
|
|
|
282
|
+
# --- smoke (pipeline dry run) ---
|
|
283
|
+
# Sends synthetic requests (no DB writes) to both LLM stages:
|
|
284
|
+
# --classify-only fast path (~30s max) called automatically from cmd_doctor
|
|
285
|
+
# (no flag) full run: classification + worklog synthesis
|
|
286
|
+
|
|
287
|
+
_smoke_read_env() {
|
|
288
|
+
local key="$1" env_file="${REPO_ROOT}/.env"
|
|
289
|
+
[[ -f "$env_file" ]] || return 0
|
|
290
|
+
grep -E "^${key}=" "$env_file" 2>/dev/null | tail -1 | cut -d= -f2- || true
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
_smoke_row() { # glyph ansi-color label detail
|
|
294
|
+
local glyph="$1" color="$2" label="$3" detail="${4:-}"
|
|
295
|
+
if [[ -t 1 ]]; then
|
|
296
|
+
printf " \033[%sm%s\033[0m %-26s \033[2m%s\033[0m\n" "$color" "$glyph" "$label" "$detail"
|
|
297
|
+
else
|
|
298
|
+
printf " %s %-26s %s\n" "$glyph" "$label" "$detail"
|
|
299
|
+
fi
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
_smoke_remedy() {
|
|
303
|
+
local msg="$1"
|
|
304
|
+
if [[ -t 1 ]]; then printf " \033[2m→ %s\033[0m\n" "$msg"
|
|
305
|
+
else printf " → %s\n" "$msg"; fi
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
cmd_smoke() {
|
|
309
|
+
local classify_only=0
|
|
310
|
+
[[ "${1:-}" == "--classify-only" ]] && classify_only=1
|
|
311
|
+
|
|
312
|
+
local mlx_port
|
|
313
|
+
mlx_port="$(_smoke_read_env MLX_SERVER_PORT)"
|
|
314
|
+
mlx_port="${mlx_port:-7823}"
|
|
315
|
+
local base="http://127.0.0.1:${mlx_port}"
|
|
316
|
+
local classify_timeout=60
|
|
317
|
+
[[ $classify_only -eq 1 ]] && classify_timeout=30
|
|
318
|
+
local all_ok=1
|
|
319
|
+
|
|
320
|
+
if [[ -t 1 ]]; then
|
|
321
|
+
printf "\n \033[36m▸ smoke (pipeline dry run)\033[0m\n"
|
|
322
|
+
printf " \033[2m%s\033[0m\n" "════════════════════════════════════════════════════════"
|
|
323
|
+
else
|
|
324
|
+
printf "\n ▸ smoke (pipeline dry run)\n"
|
|
325
|
+
printf " %s\n" "════════════════════════════════════════════════════════"
|
|
326
|
+
fi
|
|
327
|
+
|
|
328
|
+
# Quick reachability probe — if the server isn't up, nothing else can run.
|
|
329
|
+
local reach_ok=0
|
|
330
|
+
set +e
|
|
331
|
+
curl -sf --max-time 5 "${base}/health" >/dev/null 2>&1 && reach_ok=1
|
|
332
|
+
set -e
|
|
333
|
+
if [[ $reach_ok -eq 0 ]]; then
|
|
334
|
+
_smoke_row "✗" "31" "mlx reachable" "server not responding at ${base}"
|
|
335
|
+
_smoke_remedy "meridian start (or: meridian logs mlx-server)"
|
|
336
|
+
echo ""
|
|
337
|
+
return 1
|
|
338
|
+
fi
|
|
339
|
+
|
|
340
|
+
# Stage 1: classification smoke.
|
|
341
|
+
# POST /classify takes {"input":"..."} — pure model inference, zero DB access.
|
|
342
|
+
local t0 classify_resp classify_ok=0
|
|
343
|
+
t0=$SECONDS
|
|
344
|
+
set +e
|
|
345
|
+
classify_resp="$(curl -sf --max-time "${classify_timeout}" \
|
|
346
|
+
-X POST "${base}/classify" \
|
|
347
|
+
-H "Content-Type: application/json" \
|
|
348
|
+
-d '{"input":"App: Xcode\nWindow: ContentView.swift — MyApp\nOCR: func body: some View { Text(\"Hello World\") }\nDuration: 600s"}' \
|
|
349
|
+
2>/dev/null)"
|
|
350
|
+
local classify_curl_rc=$?
|
|
351
|
+
set -e
|
|
352
|
+
local classify_elapsed=$(( SECONDS - t0 ))
|
|
353
|
+
|
|
354
|
+
if [[ $classify_curl_rc -ne 0 || -z "$classify_resp" ]]; then
|
|
355
|
+
_smoke_row "✗" "31" "classification" "no response from /classify (timeout or error)"
|
|
356
|
+
_smoke_remedy "check: meridian logs mlx-server"
|
|
357
|
+
all_ok=0
|
|
358
|
+
else
|
|
359
|
+
local stype conf
|
|
360
|
+
stype="$(printf '%s' "$classify_resp" | grep -o '"session_type":"[^"]*"' | cut -d'"' -f4)" || stype=""
|
|
361
|
+
conf="$(printf '%s' "$classify_resp" | grep -o '"confidence":[0-9.]*' | cut -d: -f2)" || conf="?"
|
|
362
|
+
if [[ -n "$stype" ]]; then
|
|
363
|
+
_smoke_row "✓" "32" "classification" "${classify_elapsed}s session_type=${stype} conf=${conf}"
|
|
364
|
+
classify_ok=1
|
|
365
|
+
else
|
|
366
|
+
_smoke_row "✗" "31" "classification" "response did not parse — got: ${classify_resp:0:80}"
|
|
367
|
+
_smoke_remedy "restart MLX server: meridian dev mlx (or: meridian restart)"
|
|
368
|
+
all_ok=0
|
|
369
|
+
fi
|
|
370
|
+
fi
|
|
371
|
+
|
|
372
|
+
# Fast path (called from cmd_doctor): stop here.
|
|
373
|
+
if [[ $classify_only -eq 1 ]]; then
|
|
374
|
+
echo ""
|
|
375
|
+
[[ $classify_ok -eq 1 ]]
|
|
376
|
+
return
|
|
377
|
+
fi
|
|
378
|
+
|
|
379
|
+
# Stage 2: worklog synthesis smoke.
|
|
380
|
+
# POST /synthesise_worklog with a synthetic bundle — the agno agent runs the model
|
|
381
|
+
# and returns a JiraUpdate. Nothing is written to the DB; Rust never sees this call.
|
|
382
|
+
local jira_url jira_token linear_key github_token has_pm=0
|
|
383
|
+
jira_url="$(_smoke_read_env JIRA_BASE_URL)"
|
|
384
|
+
[[ -z "$jira_url" ]] && jira_url="$(_smoke_read_env JIRA_URL)"
|
|
385
|
+
jira_token="$(_smoke_read_env JIRA_API_TOKEN)"
|
|
386
|
+
linear_key="$(_smoke_read_env LINEAR_API_KEY)"
|
|
387
|
+
github_token="$(_smoke_read_env GITHUB_TOKEN)"
|
|
388
|
+
[[ -n "$jira_url" && -n "$jira_token" ]] && has_pm=1
|
|
389
|
+
[[ -n "$linear_key" ]] && has_pm=1
|
|
390
|
+
[[ -n "$github_token" ]] && has_pm=1
|
|
391
|
+
|
|
392
|
+
if [[ $has_pm -eq 0 ]]; then
|
|
393
|
+
_smoke_row "·" "2" "worklog synthesis" "skipped — no PM credentials in .env"
|
|
394
|
+
echo ""
|
|
395
|
+
[[ $all_ok -eq 1 ]]
|
|
396
|
+
return
|
|
397
|
+
fi
|
|
398
|
+
|
|
399
|
+
# Dates are fixed to 2024-01-01 so the output is obviously synthetic.
|
|
400
|
+
local synth_bundle
|
|
401
|
+
synth_bundle='{"bundle":{"task_key":"SMOKE-1","window_start":"2024-01-01T09:00:00","window_end":"2024-01-01T09:30:00","cycle_index":0,"sessions":[{"id":1,"app_name":"Xcode","started_at":"2024-01-01T09:00:00","ended_at":"2024-01-01T09:30:00","duration_s":1800,"idle_frame_s":0,"top_titles":["ContentView.swift — MyApp"],"excerpt":"Implementing SwiftUI body layout. func body: some View { Text(\"Hello World\") }","category":"coding"}],"total_seconds":1800,"real_seconds":1800,"pm_task_title":"Implement ContentView layout"}}'
|
|
402
|
+
|
|
403
|
+
local t1 synth_resp synth_ok=0
|
|
404
|
+
t1=$SECONDS
|
|
405
|
+
set +e
|
|
406
|
+
synth_resp="$(curl -sf --max-time 120 \
|
|
407
|
+
-X POST "${base}/synthesise_worklog" \
|
|
408
|
+
-H "Content-Type: application/json" \
|
|
409
|
+
-d "$synth_bundle" \
|
|
410
|
+
2>/dev/null)"
|
|
411
|
+
local synth_curl_rc=$?
|
|
412
|
+
set -e
|
|
413
|
+
local synth_elapsed=$(( SECONDS - t1 ))
|
|
414
|
+
|
|
415
|
+
if [[ $synth_curl_rc -ne 0 || -z "$synth_resp" ]]; then
|
|
416
|
+
_smoke_row "✗" "31" "worklog synthesis" "no response from /synthesise_worklog (timeout or error)"
|
|
417
|
+
_smoke_remedy "check: meridian logs mlx-server"
|
|
418
|
+
all_ok=0
|
|
419
|
+
elif printf '%s' "$synth_resp" | grep -q '"summary"'; then
|
|
420
|
+
local bullets conf2
|
|
421
|
+
bullets="$(printf '%s' "$synth_resp" | grep -o '"text":' | wc -l | tr -d ' ')" || bullets="?"
|
|
422
|
+
conf2="$(printf '%s' "$synth_resp" | grep -o '"confidence":[0-9.]*' | cut -d: -f2)" || conf2="?"
|
|
423
|
+
_smoke_row "✓" "32" "worklog synthesis" "${synth_elapsed}s bullets=${bullets} conf=${conf2}"
|
|
424
|
+
synth_ok=1
|
|
425
|
+
else
|
|
426
|
+
_smoke_row "✗" "31" "worklog synthesis" "response missing summary — got: ${synth_resp:0:80}"
|
|
427
|
+
_smoke_remedy "restart MLX server: meridian dev mlx (or: meridian restart)"
|
|
428
|
+
all_ok=0
|
|
429
|
+
synth_ok=0 # explicitly mark unused var for clarity
|
|
430
|
+
: "$synth_ok"
|
|
431
|
+
fi
|
|
432
|
+
|
|
433
|
+
echo ""
|
|
434
|
+
[[ $all_ok -eq 1 ]]
|
|
435
|
+
}
|
|
436
|
+
|
|
273
437
|
# --- config ---
|
|
274
438
|
cmd_config() {
|
|
275
439
|
local subcmd="${1:-}"
|
|
@@ -478,6 +642,7 @@ case "$CMD" in
|
|
|
478
642
|
status) cmd_status ;;
|
|
479
643
|
logs) cmd_logs "$@" ;;
|
|
480
644
|
doctor) cmd_doctor "$@" ;;
|
|
645
|
+
smoke) cmd_smoke "$@" ;;
|
|
481
646
|
config) cmd_config "$@" ;;
|
|
482
647
|
dev) cmd_dev "$@" ;;
|
|
483
648
|
uninstall) cmd_uninstall ;;
|
|
@@ -373,6 +373,40 @@ def _flatten_message_content(content: Any) -> str:
|
|
|
373
373
|
return str(content)
|
|
374
374
|
|
|
375
375
|
|
|
376
|
+
# Apple FM context cap: 4096-token combined context window (input + output).
|
|
377
|
+
# Reserve ~1024 tokens for the response; ~3072 for the prompt → ~12 000 chars.
|
|
378
|
+
_APPLE_FM_USER_CHARS = 12_000
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
def _infer_apple_fm(msgs: list[dict], max_tokens: int) -> str: # noqa: ARG001
|
|
382
|
+
"""Infer via Apple Foundation Models from an OpenAI-style messages list.
|
|
383
|
+
|
|
384
|
+
Extracts the last system message and joins all user/assistant turns.
|
|
385
|
+
Raises on failure — callers must handle and return 500.
|
|
386
|
+
"""
|
|
387
|
+
import asyncio
|
|
388
|
+
from apple_fm_sdk import LanguageModelSession # type: ignore[import]
|
|
389
|
+
|
|
390
|
+
system = next(
|
|
391
|
+
(m["content"] for m in reversed(msgs) if m.get("role") == "system"), ""
|
|
392
|
+
)
|
|
393
|
+
user_parts = [m["content"] for m in msgs if m.get("role") in ("user", "assistant")]
|
|
394
|
+
user = "\n".join(user_parts)
|
|
395
|
+
if len(user) > _APPLE_FM_USER_CHARS:
|
|
396
|
+
user = user[:_APPLE_FM_USER_CHARS]
|
|
397
|
+
|
|
398
|
+
async def _run() -> str:
|
|
399
|
+
session = LanguageModelSession(instructions=system)
|
|
400
|
+
result = await session.respond(user)
|
|
401
|
+
return result.content if hasattr(result, "content") else str(result)
|
|
402
|
+
|
|
403
|
+
loop = asyncio.new_event_loop()
|
|
404
|
+
try:
|
|
405
|
+
return loop.run_until_complete(_run())
|
|
406
|
+
finally:
|
|
407
|
+
loop.close()
|
|
408
|
+
|
|
409
|
+
|
|
376
410
|
@app.post("/v1/chat/completions")
|
|
377
411
|
async def openai_chat_completions(req: _OAIChatRequest) -> dict:
|
|
378
412
|
"""OpenAI ChatCompletions-shaped wrapper around the MLX model.
|
|
@@ -409,7 +443,11 @@ async def openai_chat_completions(req: _OAIChatRequest) -> dict:
|
|
|
409
443
|
temperature = req.temperature if req.temperature is not None else 0.3
|
|
410
444
|
max_tokens = req.max_tokens if req.max_tokens else 2048
|
|
411
445
|
|
|
446
|
+
from agents.llm_selector import APPLE_INTELLIGENCE_ID
|
|
447
|
+
|
|
412
448
|
def _generate() -> str:
|
|
449
|
+
if m._resolve_model_id() == APPLE_INTELLIGENCE_ID:
|
|
450
|
+
return _infer_apple_fm(msgs, max_tokens)
|
|
413
451
|
model = m._get_model()
|
|
414
452
|
return model(
|
|
415
453
|
Chat(msgs),
|
|
@@ -501,14 +539,45 @@ async def summarise(req: _SummariseRequest) -> _SummariseResponse:
|
|
|
501
539
|
if m is None:
|
|
502
540
|
raise HTTPException(status_code=503, detail="MLX model is still loading")
|
|
503
541
|
|
|
504
|
-
from
|
|
505
|
-
from outlines.inputs import Chat
|
|
542
|
+
from agents.llm_selector import APPLE_INTELLIGENCE_ID
|
|
506
543
|
|
|
507
544
|
messages = [
|
|
508
545
|
{"role": "system", "content": req.system or _SUMMARISE_DEFAULT_SYSTEM},
|
|
509
546
|
{"role": "user", "content": req.transcript},
|
|
510
547
|
]
|
|
511
548
|
|
|
549
|
+
if m._resolve_model_id() == APPLE_INTELLIGENCE_ID:
|
|
550
|
+
# outlines FSM decoding is incompatible with Foundation Models.
|
|
551
|
+
# Ask Apple FM for JSON directly; strip fences and retry once on parse error.
|
|
552
|
+
_JSON_HINT = (
|
|
553
|
+
"\n\nRespond ONLY with a JSON object — no markdown, no explanation: "
|
|
554
|
+
'{"summary": "<string>", "blockers": ["<string>", ...]}'
|
|
555
|
+
)
|
|
556
|
+
|
|
557
|
+
def _generate_fm() -> _SummarySchema:
|
|
558
|
+
fm_msgs = [
|
|
559
|
+
{"role": "system", "content": messages[0]["content"] + _JSON_HINT},
|
|
560
|
+
{"role": "user", "content": messages[1]["content"]},
|
|
561
|
+
]
|
|
562
|
+
raw = _infer_apple_fm(fm_msgs, req.max_tokens)
|
|
563
|
+
try:
|
|
564
|
+
return _SummarySchema.model_validate_json(raw)
|
|
565
|
+
except Exception:
|
|
566
|
+
stripped = raw.strip().removeprefix("```json").removeprefix("```").removesuffix("```").strip()
|
|
567
|
+
return _SummarySchema.model_validate_json(stripped)
|
|
568
|
+
|
|
569
|
+
from fastapi.concurrency import run_in_threadpool as _rtp
|
|
570
|
+
try:
|
|
571
|
+
obj = await _rtp(_generate_fm)
|
|
572
|
+
except Exception as exc: # noqa: BLE001
|
|
573
|
+
log.warning("summarise(apple_fm): parse error: %s", exc)
|
|
574
|
+
raise HTTPException(status_code=500, detail=str(exc)) from exc
|
|
575
|
+
log.info("summarise(apple_fm): out_chars=%d blockers=%d", len(obj.summary), len(obj.blockers))
|
|
576
|
+
return _SummariseResponse(summary=obj.summary.strip(), blockers=obj.blockers)
|
|
577
|
+
|
|
578
|
+
from mlx_lm.sample_utils import make_sampler
|
|
579
|
+
from outlines.inputs import Chat
|
|
580
|
+
|
|
512
581
|
def _generate() -> str:
|
|
513
582
|
model = m._get_model()
|
|
514
583
|
return model(
|
package/services/pyproject.toml
CHANGED
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "meridian-agents"
|
|
7
|
-
version = "1.
|
|
7
|
+
version = "1.24.1"
|
|
8
8
|
description = "Meridian agents — hermes task linking and Jira progress updates for meridian.db"
|
|
9
9
|
requires-python = ">=3.11"
|
|
10
10
|
authors = [{ name = "Meridiona" }]
|
package/ui.tar.gz
DELETED
|
Binary file
|