<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://rdmarsh.github.io/elm/feed.xml" rel="self" type="application/atom+xml" /><link href="https://rdmarsh.github.io/elm/" rel="alternate" type="text/html" /><updated>2026-11-04T00:40:25+00:00</updated><id>https://rdmarsh.github.io/elm/feed.xml</id><title type="html">elm</title><subtitle>LogicMonitor tools that look but don&apos;t touch</subtitle><author><name>David Marsh</name></author><entry><title type="html">designing a cli for ai assistants</title><link href="https://rdmarsh.github.io/elm/2026/10/31/designing-a-cli-for-ai-assistants.html" rel="alternate" type="text/html" title="designing a cli for ai assistants" /><published>2026-10-31T00:00:00+00:00</published><updated>2026-10-31T00:00:00+00:00</updated><id>https://rdmarsh.github.io/elm/2026/10/31/designing-a-cli-for-ai-assistants</id><content type="html" xml:base="https://rdmarsh.github.io/elm/2026/10/31/designing-a-cli-for-ai-assistants.html"><![CDATA[<p>tldr; More and more commands in a terminal are typed by an AI assistant, not
a person. <a href="https://github.com/rdmarsh/elm">elm</a>, my command-line tool for
<a href="https://www.logicmonitor.com">LogicMonitor</a> (a SaaS monitoring platform),
already suited that: it has been read-only from the start. So we
added what an assistant needs on top: short docs it can afford to read (<code class="language-plaintext highlighter-rouge">--ai</code>
and <code class="language-plaintext highlighter-rouge">--info</code>), errors that say how to fix the mistake, no silent failures, and
times written the way people say them, not as epochs. Most of it made elm
better for people too.</p>

<p>Most command-line tools assume a person who reads the manual once, remembers
it all, and notices when something looks wrong. An AI assistant does none of
that. It starts every session knowing nothing about your tool, pays for every
word it reads, and guesses confidently when it isn’t sure.</p>

<p>Here’s what matters, and what we added.</p>

<h2 id="1-make-the-safe-thing-the-only-thing">1. make the safe thing the only thing</h2>

<p>elm has always been read-only. It only sends GET requests, so nothing an assistant types
can change a monitoring portal. I decided that long before AI came into it,
and it’s still the most important decision.</p>

<p>Even though the LogicMonitor API hides a lot of details, read-only still
leaves a lot to read, like users, API tokens and contact details. So a credential profile can list the commands it allows:</p>

<div class="language-ini highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="py">allowed_commands</span> <span class="p">=</span> <span class="s">['DeviceList', 'Alert*', 'SDTList']</span>
</code></pre></div></div>

<p>Anything else stops before a request is sent:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ elm -p ai AdminList
Error: AdminList is not allowed. Profile 'ai' allows only: ...
No request was sent.
To allow it, add 'AdminList' to allowed_commands in ~/.config/logicmonitor/credentials/ai.ini.
If you are a script or an AI assistant: do not retry with another profile and do not
edit that file. Tell the person which command you need and why, and show them what you
would have run:
  elm -p ai AdminList
</code></pre></div></div>

<p>The last part is there because assistants are resourceful. Told no, the
obvious next move is another profile, so the error asks it to come back to
the person instead.</p>

<p>It exits with its own code (3), so a script can tell “not allowed” apart from
“failed”. The repo has a suggested profile for assistants with about 45
commands for everyday questions, and a comment on each one I left out saying
why.</p>

<p>This is a guard rail, not a security boundary. What really limits access is
the API token’s role in LogicMonitor, and the docs say so. But most of what
goes wrong is a wrong guess, and a guard rail stops those.</p>

<h2 id="2-give-it-a-manual-it-can-afford">2. give it a manual it can afford</h2>

<p>A person reads <code class="language-plaintext highlighter-rouge">--help</code> once. An assistant reads it every session, and every
character costs tokens and attention. So elm has two short manuals written
for that reader:</p>

<ul>
  <li><strong><code class="language-plaintext highlighter-rouge">elm --ai</code></strong> is a compact quick-start guide: how the flags work, the mistakes
assistants actually make, and how to keep output small (ask for only the
fields you need, count instead of fetching rows to count them).</li>
  <li><strong><code class="language-plaintext highlighter-rouge">elm COMMAND --info</code></strong> describes one command: its parameters, every field
it returns, and notes from testing. It needs no credentials and makes no API
call, so an assistant can look things up as often as it likes.</li>
</ul>

<p>The idea is it looks up what it needs when it needs it, instead of loading
everything up front. This saves on token usage and cost.</p>

<h2 id="3-write-errors-that-say-how-to-fix-it">3. write errors that say how to fix it</h2>

<p>elm has several short flags that mean different things before and after the
command name. <code class="language-plaintext highlighter-rouge">-f</code> is the output format before it and the field list after
it. People learn this once. Assistants get it wrong all the time.</p>

<p>A misplaced flag used to fail with a bare <code class="language-plaintext highlighter-rouge">no such option: -c</code>. That gave
nobody anything to act on, and assistants kept writing <code class="language-plaintext highlighter-rouge">elm -c AlertList</code>.
Now every error like that ends with the fix:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Hint: -c is a command option: put it after the command name, e.g. elm AlertList -c
</code></pre></div></div>

<p>The hints are built from the options elm defines, so new flags get them for
free. The <code class="language-plaintext highlighter-rouge">--ai</code> guide adds one more line: <em>read the hint and move the flag;
do not drop it.</em></p>

<h2 id="4-refuse-what-would-quietly-go-wrong">4. refuse what would quietly go wrong</h2>

<p>An error is annoying. A wrong answer with no error is worse, because an
assistant will report it as fact. The LogicMonitor API is full of these.</p>

<ul>
  <li>
    <p><strong>A misspelt field.</strong> Filter AlertList on <code class="language-plaintext highlighter-rouge">sevrity</code> and the API ignores the
filter and returns every alert. DeviceList matches nothing instead. elm now
checks each field against the API’s own field list first:</p>

    <div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ elm AlertList -F sevrity:4
Error: ... no field 'sevrity' in AlertList (did you mean severity? ...)
</code></pre></div>    </div>
  </li>
  <li><strong>An operator that isn’t one.</strong> <code class="language-plaintext highlighter-rouge">displayName:~web01</code> looks fine, but it’s
read as <code class="language-plaintext highlighter-rouge">:</code> with the value <code class="language-plaintext highlighter-rouge">~web01</code>, so it matches nothing. A small local
model I was testing used it in 4 answers out of 28, and each time reported
the device as missing. That’s refused now too.</li>
  <li><strong>A flag in the wrong place.</strong> <code class="language-plaintext highlighter-rouge">elm DeviceList -f csv</code> asked for a field
called “csv”. It’s refused before any request is sent. <code class="language-plaintext highlighter-rouge">elm -o 2000
DeviceList</code> writes the first page to a file called <code class="language-plaintext highlighter-rouge">2000</code>. That’s valid, so
it still runs, but with a warning, because it’s almost never what was meant.</li>
  <li><strong>A failed request.</strong> elm has always had <code class="language-plaintext highlighter-rouge">--halt-on-api-error</code>, and since
September, exit code 3 for a command the profile doesn’t allow. But with one
profile, a request that failed (a TLS-inspecting proxy, a 403) printed its
error and still exited 0 with nothing on stdout. To a script, that looks
exactly like “no results”. Samara, the question page built on elm, told
someone a device didn’t exist when the lookup had never got through. Now a
failed request exits 1.</li>
</ul>

<p>Each of these took a wrong answer to find. None of them were new problems,
an assistant just hit them more often than I ever had.</p>

<h2 id="5-let-it-write-times-the-way-people-say-them">5. let it write times the way people say them</h2>

<p>The API wants times as epochs. An assistant had to work out “the last hour”
itself, in seconds or milliseconds depending on the field (and the API docs
rarely say which), and in the right timezone. That’s a lot of chances to be
quietly wrong.</p>

<p>Now elm does the maths:</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>elm AlertList <span class="nt">-F</span> <span class="s1">'cleared:false,startEpoch&gt;:now-1h'</span>   <span class="c"># started in the last hour</span>
elm AlertList <span class="nt">-F</span> <span class="s1">'startEpoch:now-1M/M'</span>                <span class="c"># last calendar month</span>
elm SDTList <span class="nt">-F</span> <span class="s1">'startDateTime&lt;:now+7d'</span>                <span class="c"># SDTs starting in the next week</span>
</code></pre></div></div>

<p>It’s the same date maths as Elasticsearch and Grafana, so plenty of people
already know it. Dates work too (<code class="language-plaintext highlighter-rouge">2026-07</code>, <code class="language-plaintext highlighter-rouge">2026-07-14</code>). Everything is local
time, daylight saving included. For the seconds or milliseconds question, elm
asks the API for one value of that field first: anything above 10^11 can only
be milliseconds.</p>

<p>And <code class="language-plaintext highlighter-rouge">-t</code> turns epochs in the results back into local times:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ elm -t -f txt AlertList -F 'cleared:false,startEpoch&gt;:now-1h' -f startEpoch,endEpoch,monitorObjectName
  monitorObjectName      endEpoch  startEpoch
  web01                         0  2026-10-28 16:53:06 AEDT
</code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">0</code> stays a <code class="language-plaintext highlighter-rouge">0</code>, because it means “not ended”, not 1970. Fields like
<code class="language-plaintext highlighter-rouge">upTime</code> stay numbers too.</p>

<h2 id="6-write-down-what-testing-found-and-how-sure-you-are">6. write down what testing found, and how sure you are</h2>

<p>The API’s own docs aren’t always right. Some filters are silently ignored.
Some fields mean something other than their description. elm keeps notes
from testing against real portals, and <code class="language-plaintext highlighter-rouge">--info</code> shows them next to the
documented fields. Where they disagree, the tested note wins.</p>

<p>But notes can be wrong too. For months a note said LogicMonitor’s alert
severity numbers ran the other way round. It was written as if it had been
checked live, and every assistant that read it repeated it confidently. Fixing
the number wasn’t enough. The guide now tells assistants that if real results
contradict either source, they should say so, and every note records how and
when it was checked.</p>

<h2 id="7-bigger-is-not-safer">7. bigger is not safer</h2>

<p>It’s tempting to keep adding to AI-facing docs. Each addition feels like it
can only help. It doesn’t. A longer file costs more on every question, and it
collects contradictions and stale claims nobody re-reads.</p>

<p>So elm’s AI-facing text has budgets, checked by the build:</p>

<ul>
  <li>The knowledge file, read before every question, has to stay under 8,000
characters.</li>
  <li>Each command’s <code class="language-plaintext highlighter-rouge">--info</code> has to stay under 16,000.</li>
  <li>Each fact has one home. A rule about one command goes in that command’s
notes, a rule across commands in the knowledge file, and a longer recipe in
the examples. Never the same fact in two places.</li>
</ul>

<p>If something goes over budget the build fails, and something else has to be
trimmed, merged or deleted first.</p>

<h2 id="8-keep-secrets-and-other-peoples-words-out-of-reach">8. keep secrets, and other people’s words, out of reach</h2>

<p>elm can print the exact request it would send, including a signed
authorization header. That’s handy for debugging, and exactly what should
never end up in an assistant’s chat log.</p>

<p>So <a href="https://github.com/rdmarsh/elm/tree/main/samara">Samara</a> lets the model
pick the command, filters and fields, but not elm’s global flags. It also
strips fields that can carry secrets, and personal contact details, before
the model sees any rows.</p>

<p>Then I found the model believing what it read. Anyone who can edit a device
can write in its description or a property. I left a note on a test device
saying it had been decommissioned. A small model repeated that as fact,
twice, about a device that was up, whatever the prompt said. So now the model never reads
descriptions, comments or property values at all. The person asking still
sees every value, in a table. The model just doesn’t get to act on them.</p>

<p>I’d rather decide what the model can reach than trust it to behave.</p>

<h2 id="9-answer-to-the-name-it-guesses">9. answer to the name it guesses</h2>

<p>Most elm commands take their id as <code class="language-plaintext highlighter-rouge">--id</code>. A few use the name the API gives
it instead: <code class="language-plaintext highlighter-rouge">DeviceDatasourceList</code> wants <code class="language-plaintext highlighter-rouge">--deviceId</code>, and
<code class="language-plaintext highlighter-rouge">DeviceGroupPropertyList</code> wants <code class="language-plaintext highlighter-rouge">--gid</code>.</p>

<p>elm takes its option names straight from the API, and I’d learnt the odd
ones long ago. The assistant helping me build Samara hadn’t. It had seen
<code class="language-plaintext highlighter-rouge">--id</code> on 87 commands, so it guessed it on the rest, in a command it gave me
to run:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ elm DeviceDatasourceList --id 8810
Error: no such option: --id
</code></pre></div></div>

<p>So a command whose only parameter is an id now takes <code class="language-plaintext highlighter-rouge">--id</code> too. <code class="language-plaintext highlighter-rouge">--deviceId</code>
still works, and <code class="language-plaintext highlighter-rouge">--help</code> shows both:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  --deviceId, --id INTEGER        [required]
</code></pre></div></div>

<p>Both at once (<code class="language-plaintext highlighter-rouge">--deviceId 123 --id 456</code>) is refused. Left alone, the
command would have quietly used the last one.</p>

<p>That’s 15 commands. Where a command takes two ids, like <code class="language-plaintext highlighter-rouge">DeviceDatasourceById
--deviceId N --id N</code>, it stays strict, because there the name is what says
which id is which.</p>

<p>Every wrong guess costs an assistant a failed call and another go. If it’s going
to guess, we may as well meet it half way and make it easier for it.</p>

<h2 id="what-it-adds-up-to">what it adds up to</h2>

<p>None of this is clever. It’s the same stuff that makes any command-line tool
good: safe defaults, short docs, helpful errors, no silent failures, honest
notes. The difference is an AI user finds every weak spot quickly, over and
over, and never learns from last week.</p>

<p>Building for that reader made elm better for people too. The hints, <code class="language-plaintext highlighter-rouge">--info</code>,
<code class="language-plaintext highlighter-rouge">now-1h</code> and <code class="language-plaintext highlighter-rouge">-t</code> are just as useful at 2am when you’re the one guessing.</p>

<p>elm is open source: <a href="https://github.com/rdmarsh/elm">github.com/rdmarsh/elm</a>.
The <a href="https://rdmarsh.github.io/elm/2026/10/12/introducing-elm.html">first post</a>
gives an overview, and <a href="https://github.com/rdmarsh/elm/blob/main/ai.md">ai.md</a>
covers how I work with an AI assistant day to day.</p>

<p><em>LOGICMONITOR is a trademark of LogicMonitor, Inc. elm is an independent
project and is not affiliated with LogicMonitor.</em></p>]]></content><author><name>David Marsh</name></author><summary type="html"><![CDATA[who never read the manual]]></summary></entry><entry><title type="html">introducing elm</title><link href="https://rdmarsh.github.io/elm/2026/10/12/introducing-elm.html" rel="alternate" type="text/html" title="introducing elm" /><published>2026-10-12T00:00:00+00:00</published><updated>2026-10-12T00:00:00+00:00</updated><id>https://rdmarsh.github.io/elm/2026/10/12/introducing-elm</id><content type="html" xml:base="https://rdmarsh.github.io/elm/2026/10/12/introducing-elm.html"><![CDATA[<p>tldr; I built <a href="https://github.com/rdmarsh/elm">elm</a>, a command-line tool that
gets data out of <a href="https://www.logicmonitor.com">LogicMonitor</a> (a SaaS
monitoring platform) in one command, in whatever format you need next. It only reads, so it can’t change anything in your portal.</p>

<p>If you look after LogicMonitor, you’ve undoubtedly been asked something like
this:</p>

<ul>
  <li>Which devices are in downtime right now, and who put them there?</li>
  <li>Which alerts have been open and unacknowledged for more than a week?</li>
  <li>Is our pre-production portal set up the same way as production?</li>
</ul>

<p>The answer is in the portal somewhere. Getting it out usually means clicking
through screens, exporting what you can, and pasting it into a spreadsheet. Or
you write a script against the REST API and spend the first hour on HMAC
signing, paging and working out which filters actually work.</p>

<p>I built elm so the answer is one command.</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># open critical alerts, newest first, as a table</span>
elm <span class="nt">-f</span> pipe AlertList <span class="nt">-s0</span> <span class="nt">-S</span> startEpoch <span class="nt">-F</span> cleared:false,severity:4 <span class="se">\</span>
    <span class="nt">-f</span> monitorObjectName,dataPointName,alertValue

<span class="c"># the same query as CSV for a spreadsheet: change one flag</span>
elm <span class="nt">-f</span> csv AlertList <span class="nt">-s0</span> <span class="nt">-S</span> startEpoch <span class="nt">-F</span> cleared:false,severity:4 <span class="se">\</span>
    <span class="nt">-f</span> monitorObjectName,dataPointName,alertValue
</code></pre></div></div>

<h2 id="what-elm-is">what elm is</h2>

<p>elm is a command-line tool for reading data out of LogicMonitor. It handles
the authentication, paging and filtering, and prints the result in the format
you need: CSV for a spreadsheet, JSON for a script, Markdown or HTML for a
report, Jira markup for a ticket. There are over 20 formats, and you switch
between them with <code class="language-plaintext highlighter-rouge">-f</code>.</p>

<p>It covers the whole API: more than 180 commands, one for each read endpoint,
from <code class="language-plaintext highlighter-rouge">DeviceList</code> and <code class="language-plaintext highlighter-rouge">AlertList</code> to <code class="language-plaintext highlighter-rouge">AuditLogList</code>, <code class="language-plaintext highlighter-rouge">SDTList</code>, collectors,
websites and LogicModules.</p>

<h2 id="read-only-on-purpose">read-only, on purpose</h2>

<p>elm only sends GET requests. It can’t create, change or delete anything in
your portal. The worst it can do is run a slow query.</p>

<p>Most of the other decisions follow from that one. It means elm is safe to hand
to someone new to LogicMonitor, safe to put in a cron job, and safe to give to
an AI assistant.</p>

<h2 id="built-from-the-apis-own-description">built from the API’s own description</h2>

<p>I don’t write elm’s commands by hand. The build reads LogicMonitor’s published
API spec and generates a command for each endpoint, with its parameters, help
text and every field it returns. When LogicMonitor adds an endpoint, a rebuild
picks it up.</p>

<p>The spec isn’t always right, so elm also carries notes from testing against
real portals: filters the API quietly ignores, fields that don’t mean what the
docs say. <code class="language-plaintext highlighter-rouge">elm AlertList --info</code> shows them next to the documented fields, and
doesn’t need credentials.</p>

<h2 id="comparing-portals">comparing portals</h2>

<p>If you look after more than one portal (production and pre-production, or one
per region), keeping them in step is a job that never quite finishes. elm runs
the same query on several portals at once and can show only what differs:</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>elm <span class="nt">-p</span> prod,preprod <span class="nt">-d</span> <span class="nt">-f</span> csv DeviceGroupList <span class="nt">-f</span> name,appliesTo
</code></pre></div></div>

<p>The output lists only the groups that exist on one portal and not the other,
or whose rules differ. Like <code class="language-plaintext highlighter-rouge">diff</code>, it exits 0 when they match, so it works in
a scheduled check.</p>

<h2 id="plain-english-questions">plain-English questions</h2>

<p>The newest piece is
<a href="https://github.com/rdmarsh/elm/tree/main/samara">Samara</a>: a small web page where someone who has never
used a command line can type “which Windows devices have alerts?” and get back
an answer, a sortable table and the elm queries that produced it.</p>

<p>Everything goes through elm, so it can only read. It also uses its own
credential profile, limited to a list of commands allowed by the credential file. Secrets and
contact details are removed before the model sees any data, and the model never
reads descriptions, notes or logins at all: you see them in the tables, it
doesn’t.</p>

<h2 id="small-tools-one-job-each">small tools, one job each</h2>

<p>elm does one thing: get the data out. Everything else is a separate script on
top of it, the Unix way: backups of portal config, a collector capacity report,
a check for LogicModule updates, a PDF report comparing portals. They live in
<a href="https://github.com/rdmarsh/elm/tree/main/tools"><code class="language-plaintext highlighter-rouge">tools/</code></a> next to elm, and
each one pipes elm’s output into the next step.</p>

<h2 id="how-it-was-built">how it was built</h2>

<p>elm started in 2021 as a weekend script that got out of hand, spun off from my
earlier work on <a href="https://github.com/rdmarsh/zahar">zahar</a>, a now-archived
project that did the same with Zabbix. Since then, it’s grown into a
comprehensive tool with about 600 commits. A lot of the recent work has been
done with AI coding assistants. The main thing I’ve seen is that it works best
when the project’s knowledge is stored in files, not in the chat history:
rules, decisions and test results, written down where they can be checked or
referenced. A lot of that is captured in
<a href="https://github.com/rdmarsh/elm/blob/main/ai.md">ai.md</a>.</p>

<h2 id="try-it">try it</h2>

<p>elm is free and open source under the GPL. You need Python, <code class="language-plaintext highlighter-rouge">make</code> and a
LogicMonitor API token:</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git clone https://github.com/rdmarsh/elm.git
<span class="nb">cd </span>elm
make <span class="o">&amp;&amp;</span> make <span class="nb">install</span>
</code></pre></div></div>

<p>The <a href="https://github.com/rdmarsh/elm#readme">README</a> covers setup, and
<a href="https://github.com/rdmarsh/elm/tree/main/examples">examples/</a> has recipes for
alerts, devices, collectors and more. Questions, ideas and bug reports are
welcome in the <a href="https://github.com/rdmarsh/elm/issues">issue tracker</a>.</p>

<p><em>LOGICMONITOR is a trademark of LogicMonitor, Inc. elm is an independent
project and is not affiliated with LogicMonitor.</em></p>]]></content><author><name>David Marsh</name></author><summary type="html"><![CDATA[logicmonitor data without the clicking]]></summary></entry></feed>