v0.13.0
Added
-
Full action parity across the CLI, REST, and MCP surfaces. Every cron-job
and routine action is now reachable from all three.- New CLI data commands (thin clients over the running server's REST API,
built onclap):moadim cron-jobs <create|list|get|update|replace|delete|trigger|logs>,
moadim routines <create|list|get|update|replace|delete|trigger|logs|ical>,
moadim agents, andmoadim echo <message>. They print the server's JSON
response and exit3("not running") when no daemon is reachable, matching
the existingstatus/stop/cleanupcontract. (cron/routineare
accepted as aliases.) - New MCP tools filling the gaps versus REST:
list_agents,
cron_job_logs,routine_logs,shutdown, andrestart. - New
POST /api/v1/restartroute (plus the matchingrestartMCP tool):
stops the running server and starts a fresh instance via a detached helper
process, since an in-process server cannot rebind its own port. Documented in
the OpenAPI spec.
- New CLI data commands (thin clients over the running server's REST API,
-
The MCP
healthtool now reports build provenance —version,git_sha, and
build_date— bringing it to parity withGET /api/v1/healthand
moadim --version, so an MCP client can tell exactly which build is running
rather than only seeing status, uptime, and filesystem locations (#476). -
The binary now embeds the git commit it was built from, so you can tell
exactly which build is running rather than only the released crate version
(which changes only on av*tag).moadim --versionprints
moadim <version> (<short-sha> <date>), and theGET /api/v1/healthresponse
gainedgit_shaandbuild_datefields alongsideversion.build.rs
resolves the fields from git at compile time and falls back to"unknown"
when the source isn't a git checkout (e.g. a crates.io tarball), so published
builds still compile and report sensibly (#367). -
Routines now track
last_scheduled_trigger_at(Unix seconds), the mirror of
last_manual_trigger_atfor scheduled cron firings, surfaced in the REST/OpenAPI
routine response. Because the OS crontab runs a routine's generatedrun.sh
directly — the daemon never observes a scheduled fire — the script itself stamps
the fire time into a new gitignoredscheduled.local.tomlsidecar, which the
daemon reads back on load. The sidecar is daemon-read-only and kept separate from
the manual-triggerstate.local.toml, so re-persisting a routine can't clobber a
scheduler-written timestamp. This makes scheduled vs. manual runs distinguishable
and lets you spot schedules that have never actually fired (#155). -
moadim stopaccepts a--quiet/-qflag that suppresses the human-readable
status line (moadim is shutting down/moadim is not running) while keeping
the exit-code contract (0when a server was stopped,3when none was
running), so scripts that branch on$?alone get no stdout noise. The flag is
ignored under--json, which always prints its single machine-readable object. -
moadim stop --jsonnow includes the boundaddressfield
({"running":bool,"pid":N|null,"address":"127.0.0.1:5784"}), matching
status --json's object shape exactly so both can be parsed uniformly. -
The web UI header now shows the running daemon version (e.g.
/ v0.12.0)
next to theMOADIM / CONTROLlogo. TheGET /api/v1/healthresponse gained
aversionfield (fromCARGO_PKG_VERSION) that the UI already-polled health
request surfaces, so no extra request is made. -
Routine create/update now validates and normalizes
repositoriesentries:
blank or whitespace-onlyrepositoryvalues (and blankbranchvalues when
set) are rejected with a400 Bad Requestinstead of being silently
persisted, and surviving entries are trimmed. Malformedrepositorieslists
are now caught at the API boundary rather than surfacing later as a confusing
run-time failure (#241).
Changed
- Renamed the misleading
last_triggered_atfield tolast_manual_trigger_at
on both routines and cron jobs (TOML, REST/OpenAPI, MCP tool descriptions, and
the web UI). The field was only ever updated by manual triggers, never by
scheduled cron firings, so the old name wrongly read as "never ran" for a
routine that fires on schedule but was never triggered by hand. Deserialization
accepts the legacylast_triggered_atkey via a serde alias, so existing
routine.toml/ job files still load. - Service tests no longer touch the real user crontab; they run against an
isolated test crontab seam. - moadim-generated
.gitignorefiles (job and routine) now ignore
user-specificrun.shscripts. - Enabled the
clippy::uninlined_format_argslint (deny) and inlined the
existing positional format arguments ("{}", x→"{x}") so log lines and
error messages read more directly. No behavior change.
Fixed
- The in-memory routine and cron-job stores no longer panic the request that
observes a poisoned lock. EveryMutex::lock().unwrap()on these stores was
replaced with a newLockRecover::lock_recover()extension that recovers the
guard fromPoisonError(the protectedHashMapis still structurally valid),
so one panicking handler can't cascade into every later request taking the same
lock. The twoget_mut(id).unwrap()invariant unwraps insvc_update/
svc_triggerbecameok_or(AppError::NotFound)?, removing the last panicking
unwraps from the production code paths. A new
#![cfg_attr(not(test), deny(clippy::unwrap_used))]crate lint now keeps
.unwrap()out of non-test code so the panic can't creep back in (tests still
use.unwrap()freely, where panicking is the intended failure mode). - Managed cron jobs are now re-synced to the OS crontab on daemon startup,
mirroring the routines sync that already ran. Previously the cron-job block was
only written on a job create/update/delete, so if it was lost or emptied
(manualcrontab -e/crontab -r, an OS migration, or a marker collision) every
managed job stayed silently un-fired until the next mutation — even across a
restart, while routines self-healed. The startup sync is idempotent, so it is a
no-op read on a healthy crontab. (#394) - The generated
prompt.mdno longer emits a dangling "These repositories are
relevant — clone any you need:" header with an empty bullet list when a routine
has norepositories.compose_promptnow writes a plain "You are working in
an empty directory." preamble in that case, so the agent isn't promised a repo
list with nothing under it. - Deflaked
stop_running_and_wait_force_kills_then_succeeds_when_server_goes_down:
the test raced a ~35ms window between the restart timeout (80ms) and the server
drop (130ms), so a coverage-instrumented or loaded CI run could miss the post-kill
wait_until_stoppedwindow and fail the assertion. The margins are now 300ms /
450ms, giving ~150ms of slack on each side of the deadline while still exercising
the same force-kill-then-stops path. - A malformed (present-but-unparseable) agent TOML is no longer misreported as
"agent config not found".load_agent_commandnow returns aResultwith a
distinctMissingvs.Parsefailure, so the sync/trigger skip diagnostics
name the agent and quote the underlyingtomlparse error. Creating or
updating a routine that references a malformed agent config is now rejected
with400 Bad Request(REST + MCP) at edit time instead of being silently
skipped at fire time. The missing-file case is unchanged (still skipped and
warned, with an accurate message). (#189) - Unknown paths under
/api/v1now return a JSON 404 instead of the SPA
index.htmlwith200. The nested API router had no fallback of its own, so
in axum 0.8 it inherited the outer SPA.fallback(get(index))— a typo'd or
removed endpoint answered with HTML/200, surfacing as a confusing downstream
parse error rather than a clear not-found. The API router now owns a JSON 404
fallback while the SPA fallback still serves UI routes (#270). - Crontab docs no longer claim reverse sync (crontab → moadim) runs. It is
implemented but never wired to a poller or startup hook, so manual edits to
the moadim block do not round-trip and are overwritten by the next forward
sync. The in-crontab header, README "Crontab sync" section, and module/main
docs now say so instead of promising automatic sync-back (#218). uptime_secsis now clamped against backward clock skew (saturating
subtraction) so it never underflows.- Routine create/update now validates the configured agent, rejecting unknown agents.
- The daemon now installs a logging backend at startup so
logcalls
actually emit output instead of being silently dropped. moadim statusnow reports the effective bind address instead of the
hardcoded default when a custom bind address is configured.- iCal
escape_textnow normalizes carriage returns (CR and CRLF) to\n
per RFC 5545, so generated calendar feeds no longer emit raw control
characters in escaped text. - Cron
@keyworddocumentation now matches the actual validation contract,
aligning the documented and accepted set of@-keywords. - Routine create/update now reject nonsensical field values with
400 Bad Requestinstead of silently persisting a broken routine. A blank
(empty/whitespace-only)titlepreviously produced an empty routine-origin
disclosure name and a bare"routine"slug (#226); a blankpromptmade the
routine fire forever with no task (#224); and a zerottl_secs/
max_runtime_secsinstantly reaped the run's logs or self-killed the session
(#233). All four are validated up front on bothPOST(create) andPATCH
(update), before anything is written to disk or the crontab. - Routine create/update now reject a blank or unusable
titlewith
400 Bad Request. A title must contain at least one alphanumeric character
(so empty, whitespace-only, and punctuation-only titles like"!!!"are
refused) and is capped at 200 characters. Previously such a title was accepted,
producing a nameless routine-origin disclosure (Routine name:with nothing
after it) in the workbenchCLAUDE.mdand a silent"routine"slug the user
never chose. - Route the macOS LaunchAgent
plist_path()through theMOADIM_HOME_OVERRIDEhome seam so service install/uninstall tests can no longer write to or delete the developer's real~/Library/LaunchAgents/io.moadim.daemon.plist(#214). kill_pid(the force-kill fallback in the restart path) now resolves its
executable through an opt-inMOADIM_KILL_BINseam, letting tests inject a
harmless shim instead of signalling a real PID. The default stays the platform
killer (kill/taskkill), so the existing self-contained test that kills
its own spawned child still works. (#216)