Drive the Luge AI-employee platform from the command line — boards, agents, chat, workflows, and more, built for agents like Claude Code.
Project description
luge-cli
A command-line client for the Luge AI-employee platform — built to let an agent (Claude Code) drive Luge: work kanban boards, run scheduled tasks and workflows, chat in channels / DMs / with AI agents, manage artifacts, personal skills, and inbound webhooks, and read/update tenant settings.
Commands are grouped by noun: board and card (read + work a board),
schedule (recurring agent tasks), workflow + activity (run and inspect
agent workflows and their runs), notification (your notification-center
inbox), channel / dm / agent / colleagues (group
chats, direct messages, talking to an AI, and the people directory), artifact
(conversation deliverables), skill (your personal Luge skills), webhook
(inbound webhook endpoints), settings (tenant config), auth (local
credentials), and profile (switching between saved backends). No card
deletion, no board/column management.
Install
From PyPI:
uv tool install luge-cli # recommended — installs the `luge-cli` command globally
# or: pipx install luge-cli
# or: pip install luge-cli
Then install the bundled Claude Code skill so an agent knows how to drive the CLI:
luge-cli claude skill install
With uv tool, make sure uv's bin dir is on your PATH (once): uv tool update-shell.
Upgrade with uv tool upgrade luge-cli (or pip install -U luge-cli).
From source (development)
cd luge-cli
make install # editable uv tool install + the skill
--editable means git pull updates the command with no reinstall. make help
lists every target.
Configure
Needs the Luge API base URL and a tenant API key (luge_…). The base URL is the
full API base, including any deployment prefix (usually /api).
luge-cli auth init --url https://luge.example.com/api --token luge_xxx
luge-cli auth show # verify (token is masked)
LUGE_URL / LUGE_TOKEN environment variables override the stored file. The
config lives at ${XDG_CONFIG_HOME:-~/.config}/luge-cli/config.toml, written
0600 — never commit it.
Profiles
Credentials are stored under named profiles, so a dev and a prod backend can coexist and you switch instead of retyping:
luge-cli profile add dev --url http://localhost:8000/api --token luge_dev_xxx
luge-cli profile add prod --url https://luge.example.com/api --token luge_prod_xxx
luge-cli profile list # all profiles, `*` marks the active one
luge-cli profile use prod # switch the persistent default
luge-cli --profile dev board list # one-off override (flag goes before the subcommand)
LUGE_CLI_PROFILE=dev luge-cli board list # per-shell/session override
Selection precedence: --profile > LUGE_CLI_PROFILE > profile use > the
sole stored profile. LUGE_URL / LUGE_TOKEN still override whatever profile
was selected, field by field. auth init stays the first-run bootstrap: it
writes whichever profile the run targets (default on a fresh machine). A
config file from before profiles existed keeps working — it reads as a profile
named default and is rewritten in the new shape on the next write.
Output for agents and scripts
Every command that prints data takes --json. LUGE_CLI_JSON=1 turns it on for
all of them at once, so an agent or a CI job sets it once instead of remembering
a flag on every call:
export LUGE_CLI_JSON=1
luge-cli table row list Expenses # JSON, no flag
luge-cli table row list Expenses --json # same, explicit
LUGE_CLI_JSON=0 luge-cli table list # human view, whatever the environment says
This matters more than it looks: the human views are grids that fit themselves
to the terminal and clip what does not fit — and outside a terminal that width
is 80 columns, so a piped table row list silently ellipsizes long cells. --json is
never truncated. If something reads the output rather than a person, give it
JSON.
(It is LUGE_CLI_JSON, not LUGE_JSON: LUGE_* is the platform's own
namespace — this only concerns the CLI's output.)
A second nicety is on by default: after any command that talked to the API,
one line on stderr flags unread notifications (🔔 3 unread notification(s) — luge-cli notification list). In JSON mode — --json on the command or
LUGE_CLI_JSON=1 — the bell matches the form:
{"unread_notifications": 3, "hint": "luge-cli notification list --unread --json"}.
The streams stay separate on purpose: stdout is the command's JSON document,
stderr is the bell's — each parses on its own; don't merge them with 2>&1
before parsing. It rings only after successful,
networked commands, never for the notification group itself (you are looking
at the inbox), and any failure of the probe is silent — it can never break a
command or pollute piped/--json stdout. Acknowledging (notification ack) is
what stops the re-alert.
Turning it off, most specific wins: --no-notify (root flag, one run) >
LUGE_CLI_NOTIFY=0 (shell/session) > luge-cli notification disable
(persisted in the config file; notification enable restores the default).
Use
Commands are grouped by noun (board, card, auth, skill):
luge-cli board list # boards you can access
luge-cli board show Roadmap # columns + cards (id or name substring)
luge-cli board show Roadmap --limit 50 # show more of each column
luge-cli card list Roadmap --status open --tag bug # filter by tag/color/column/assignee/priority
luge-cli card show ROL-17 # one card + comment thread + artifacts
luge-cli card history ROL-17 # who did what to the card, newest first
luge-cli card mine # cards assigned to you
luge-cli card search "login" --status open # free-text across your boards
luge-cli card create Roadmap "In progress" "Fix login bug" --priority high --tag bug
luge-cli card comment ROL-17 "Picking this up"
luge-cli card move ROL-17 "In progress"
luge-cli card move-board ROL-17 "Roadmap" # transfer to another board (card is renumbered)
luge-cli card done ROL-17 # sets the completed flag (not the column)
luge-cli card reopen ROL-17
luge-cli card attach ROL-17 ./report.pdf # upload a local file and link it to the card
luge-cli card detach ROL-17 3 # detach attachment #3 (or its link id)
luge-cli card read ROL-17 1 # read attachment #1's content
luge-cli <group> <command> --json # structured output for scripts/agents
A card is named by the display_id every listing prints (ROL-17) or its uuid —
its board is worked out for you, so you can paste back what you just read. Add
--board to name a card by a title substring instead (card show "login" --board Roadmap), or to settle the rare case of two boards issuing the same id.
card list and card create still take a board first: they act on a board, not
on a card.
Boards, columns and (with --board) card titles are referenced by id or a
unique name substring; an ambiguous reference is a loud error, never a silent
guess.
Tables
A table is a small typed data store — the "Tables" surface in the app. table
manages the table and its columns; table row reads and writes the data. The
rows sit under the table rather than beside it because a row has no id you can
paste on its own — the API addresses one as /datasets/{id}/rows/{row_id}, so
every row command names a table. (A card earns a group of its own by naming
itself: card show ROL-17. A row never can.)
luge-cli table list # tables you can see
luge-cli table show Expenses # column spec + row count
luge-cli table create Expenses \
--column 'label:text!' \
--column 'amount:number|Amount (CAD)' \
--column 'status:select!=open,paid' \
--column 'due:date' \
--column 'who:user=$user'
luge-cli table update Expenses --name Costs -d "Team costs"
luge-cli table delete Expenses # rows included (-y to skip the prompt)
luge-cli table row list Expenses # every row, paginated
luge-cli table row list Expenses -w 'status=open' -w 'amount>20' --sort amount --desc
luge-cli table row show Expenses 48b3e75d # one row in full, a cell per line
luge-cli table row create Expenses -s 'label=Taxi' -s 'amount=42.50' -s 'status=open'
luge-cli table row update Expenses 48b3e75d -s 'status=paid' # short id, as printed
luge-cli table row update Expenses 48b3e75d --clear due # empty an optional cell
luge-cli table row delete Expenses 48b3e75d
# Relational columns — pass the whole spec as JSON (link / lookup / rollup)
luge-cli table create Affaires --columns-json '[
{"key":"titre","label":"Titre","type":"text","required":true},
{"key":"entreprise","label":"Entreprise","type":"link","target_dataset_id":"<uuid>","multiple":false},
{"key":"dom","label":"Domaine","type":"lookup","via":"entreprise","target_field":"domaine"}
]'
luge-cli table row create Affaires -s 'titre=Renouvellement' --link-by 'entreprise=nom:Acme'
luge-cli table row create Affaires -s 'titre=Onboarding' -s 'entreprise=["<row-uuid>"]'
A column is key:type, plus ! to make it required, =a,b,c for a
select's options, =$variable to have Luge fill it in, and |Label to name it
(the label defaults to a readable form of the key — unit_price → "Unit
price"). Types: text, number, date, boolean, select, user.
| spec | means |
|---|---|
title:text! |
required text |
amount:number|Amount (CAD) |
optional number, explicit label |
status:select!=open,paid |
required select (the ! rides on the type) |
who:user=$user |
filled with whoever inserts the row |
logged:date=$datetime |
filled with the insert time |
Variable columns ($user, $display_name, $datetime, $email) are filled in
by Luge, so table row create never asks for them.
Relational columns — link, lookup, rollup — carry three or four fields
each, more than the compact grammar holds, so pass the whole spec as JSON with
--columns-json '[…]' or --columns-file spec.json (the array the API validates,
sent through untouched — so a column type added later works with no CLI change).
The three column sources are mutually exclusive; each JSON form is the whole spec.
| type | JSON fields (beyond key / label / type) |
|---|---|
link |
target_dataset_id (uuid), multiple (bool, default true) |
lookup |
via (a link column's key), target_field (a key on the linked table) |
rollup |
via, agg ∈ count|sum|avg|min|max|concat, target_field (unless agg=count) |
A link cell is a list of target row ids. Write it with a bare id
(-s 'entreprise=<uuid>') or a JSON array (-s 'entreprise=["<uuid>","<uuid>"]'),
or name the target by a business value with --link-by 'entreprise=nom:Acme' —
Luge matches that against the linked table (field == value) and errors if
nothing matches. --link-by is one per link column; for several explicit targets
use -s 'entreprise=["<uuid>","<uuid>"]'. lookup and rollup cells are
computed by Luge, so --set never writes them. table row list / show print a
link cell by the linked row's name (from the response's link_labels), and
table show summarizes each relational column's wiring.
Cells are typed by that spec: --set amount=42 sends the number 42, and a
value the column cannot hold is refused before the request goes out. --where
takes key=value, key!=value, key~text (contains), and key>n / key>=n /
key<n / key<=n on numbers and dates; filters AND together. Quote any filter
using < or > so the shell does not read it as a redirection.
table row list is a grid, so it fits its cells to the terminal; table row show reads one row whole, a cell per line, without reaching for --json. A row is named by the
short id the grid prints (a unique prefix is enough) and never by its position
in a listing — a rank depends on the filters that produced it.
table update --column replaces the whole spec — pass every column you
keep. A kept key keeps its type, and dropping a column hides its data (it
returns if you re-add the key), so the CLI names what would be dropped and asks
first. table show prints the current spec.
Reads cover your own tables plus tenant-visible and shared ones; writes act on tables you own. Sharing is managed in the app.
Scheduled tasks
A scheduled task is a recurring prompt that fires an agent run. Manage them under
luge-cli schedule:
luge-cli schedule list [--enabled] # tasks you can see
luge-cli schedule show "Daily digest" # one task (id or name)
luge-cli schedule create "Daily digest" --prompt "Summarise open cards" \
--every daily --at 09:00 # see --every below
luge-cli schedule update "Daily digest" --every weekly:mon,fri --at 08:30
luge-cli schedule toggle "Daily digest" # pause / resume
luge-cli schedule run "Daily digest" # trigger a one-off run now
luge-cli schedule delete "Daily digest" # (asks to confirm; -y to skip)
--every is a compact recurrence spec (the API uses structured recurrence, not
cron): daily, hourly, weekly:mon,wed,fri, monthly:15, interval:4h,
interval:2d, once:2026-08-01T18:00. --at HH:MM sets the time(s) of day
(repeatable; defaults to 09:00). A run's history/result isn't on the task — a
triggered run surfaces under luge-cli activity (see below).
Workflows and activity
luge-cli workflow inspects, authors and runs multi-step agent workflows:
luge-cli workflow list [--trigger-type event] [--enabled] # workflow definitions
luge-cli workflow show "veille-techno" # one definition (id or name)
luge-cli workflow create "Greeter" --agent "General Agent" --prompt "Say hi and the time"
luge-cli workflow create "Complex" --graph ./graph.json --trigger-type event
luge-cli workflow update "Complex" --graph ./graph.json # only passed fields change
luge-cli workflow update "Relances" --enable # or --disable, --name, ...
luge-cli workflow delete "Greeter" # (asks to confirm; -y to skip)
luge-cli workflow trigger "veille-techno" --data topic=rust # start a run (repeatable --data)
luge-cli workflow runs "veille-techno" # a workflow's run history
luge-cli workflow run "veille-techno" <run-id> # one run in full
luge-cli workflow cancel "veille-techno" <run-id> # cancel a running run
luge-cli workflow retry "veille-techno" <run-id> # retry a failed/cancelled run
create --agent <id|name> --prompt "..." builds a linear single-agent workflow
(start → agent → end) for you; create --graph <file.json> posts an arbitrary
roomkit-graph you supply (for branches, human-review, notifications, etc.).
update patches only the fields you pass; --graph replaces the graph as a
whole — workflow show <ref> --json, edit the graph object, pass it back.
luge-cli workflow node addresses a graph node by node, so changing one
node's config no longer means round-tripping the whole graph:
luge-cli workflow node list "test table" # id, type, config keys of each node
luge-cli workflow node show "test table" function-1784 # one node whole (id or unique prefix)
luge-cli workflow node update "test table" function-1784 --set code=@snippet.js
luge-cli workflow node update "test table" function-1784 --set timeout_ms=5000 # typed by the node schema
node update patches only the keys you --set (repeatable): it fetches the
graph, merges the change into the one node's config, and PUTs the graph back
(the server validates it as a whole — there is no node endpoint). Each --set
is typed against the node type's config schema the way a row's cells are typed
against its column spec, so timeout_ms=5000 leaves as a number, not "5000".
Use key=@file to read a value from a file — the way to carry a function
node's multi-line code; an undeclared key travels as a plain string.
An event trigger fires the workflow off another surface. Pass --trigger-type event with a --trigger-config JSON that names the source_type (workflow,
scheduled_task, webhook, board, or data_table) and, optionally, filters —
e.g. fire on a new/updated row in a Luge table:
luge-cli workflow create "On new order" --agent "Ops" --prompt "Handle it" \
--trigger-type event \
--trigger-config '{"source_type":"data_table","source_data_table_id":"<table id>","events":["row_created"]}'
luge-cli activity is the cross-surface run inbox — every run (schedule,
workflow, webhook, notetaker, …), for diagnosing what fired and what failed:
luge-cli activity list [--status failed] [--kind schedule] [--search "..."]
luge-cli activity show <id> # a run's message thread + human-review steps
luge-cli activity retry <id> # re-run a failed/cancelled run
luge-cli activity stats [--days 7]
show / retry take either id a run carries: the id from activity list (its
chat room) or the execution_id — the one schedule run prints — so you can
paste back whichever id you were given. Workflow runs have no chat room; inspect
those with workflow runs instead.
Notifications
luge-cli notification reads your notification-center inbox — mentions,
card assignments, run failures, meeting summaries, membership changes. The API
key sees the inbox of the user who owns it.
luge-cli notification list # newest first; ● marks unread
luge-cli notification list --unread --json # what an agent should consume
luge-cli notification ack <id> # mark one read (id from `list`)
luge-cli notification ack --all # clear the whole inbox
The --unread filter applies within the fetched page (the API has no
server-side unread filter) — raise --limit (max 100) or page with --offset
to see more. The verb is ack, not read: card read already means display
content, and acknowledging is a write. enable / disable persist the
after-command bell switch (see above). The agent loop: bell on stderr → list --unread --json → handle → ack — acknowledging is what stops the re-alert.
Settings
luge-cli settings reads and updates tenant configuration. Most of it needs an
admin-role API key — a member key gets 403 on the memory/pii/compliance/
web-search slices and on credentials; agents, providers and the tenant read
are member-safe.
luge-cli settings get memory # memory | pii | compliance | web-search | tenant
luge-cli settings set web-search web_search_provider=brave # partial patch (only keys you pass)
luge-cli settings set memory rag_max_chunks=8 summarization_enabled=true
luge-cli settings agents # the tenant's agents (provider/model live here)
luge-cli settings agent "General Agent" # one agent's config (id or name)
luge-cli settings providers # AI provider/model catalogue
luge-cli settings credentials # configured credentials (secrets never returned)
set values are typed: true/false → bool, numbers → int/float, json:[...]
or json:{...} for lists/objects, anything else a string.
Conversations: channels, DMs, agents
Sending a message has three distinct surfaces — one command group each — plus a
colleagues directory to find who to DM.
Group chat — luge-cli channel participates in team channels. Reads and
posts are plain; the AI answers only when a message @luge-mentions it.
luge-cli channel list ; luge-cli channel browse # channels you can see / can join
luge-cli channel show general # one channel (id or name)
luge-cli channel messages general --limit 50 # message history (oldest-first)
luge-cli channel post general "ship it @luge" # @luge to invoke the agent
luge-cli channel join general ; luge-cli channel leave general
luge-cli channel create team --display "Team" --public --agent <id>
Direct message — luge-cli dm sends a 1:1 message to a colleague (human↔human,
never triggers the AI). dm send opens the DM (get-or-create) and posts in one go.
luge-cli dm send "Alex" "got a sec?" # colleague by id, email, or name
luge-cli dm list # your DMs
luge-cli dm show "Alex" --limit 50 # read the thread
Agent chat — luge-cli agent talks to an AI agent.
luge-cli agent send "Summarise the open cards" [--agent <id|name>] [--conversation <id>]
luge-cli agent send "Any blockers?" --wait --json # wait for the reply → {conversation_id, reply}
luge-cli agent list # your agent conversations
luge-cli agent show <id> --limit 50 # a conversation + its messages
By default agent send is fire-and-forget: it invokes an agent and returns the
conversation id immediately (read the reply later with agent show <id>). With
--wait it polls until the agent's text reply arrives and prints it (intermediate
tool-call steps are skipped; tune with --timeout / --interval) — the
agent-friendly "ask → answer" form, especially with --json. --agent accepts an
id or a name.
Colleagues — luge-cli colleagues is the platform people directory.
luge-cli colleagues list [--type human] # the roster
luge-cli colleagues get "Alex" # a colleague's profile (id, role, skills)
Artifacts
luge-cli artifact works with conversation artifacts (markdown/html/code
deliverables). Every artifact lives in a room — the --room id is a
conversation/channel id (shown by chat show / channel messages).
luge-cli artifact mine # your artifacts (summaries)
luge-cli artifact list --room <room-id> # a room's artifacts (with content)
luge-cli artifact show <id> # one artifact + its content
luge-cli artifact create --room <room-id> "Notes" ./notes.md --type markdown
echo "# Draft" | luge-cli artifact create --room <room-id> "Draft" - # from stdin
luge-cli artifact delete <id> # (asks to confirm; -y to skip)
There is no update-in-place — create always inserts a new artifact.
Personal skills
luge-cli skill manages your personal Luge skills — authored instruction sets
(name, description, instructions body, required tools, trigger phrases) that agents
can use, visible only to you. Reading works with any key; create/update/delete need
the skill-manage capability (an admin/owner key).
luge-cli skill list # your personal skills
luge-cli skill show <name> # description, instructions, requires, triggers
luge-cli skill create my-skill --description "..." --instructions "..." \
--require terminal_run --trigger "do the thing"
luge-cli skill create my-skill --instructions-file ./SKILL.md # body from a file
luge-cli skill update my-skill --description "..." [--instructions … --require … --trigger …]
luge-cli skill delete my-skill # (asks to confirm; -y to skip)
(Not to be confused with luge-cli claude skill, which installs this CLI's own
Claude Code skill — see Claude Code skill below.)
Inbound webhooks
luge-cli webhook manages inbound webhook endpoints — receivers Luge hosts so an
external system can drive automation. Each endpoint POSTs arriving at its
inbound_url are routed to an agent, a workflow, or a data table (or
just logged). The CLI creates/inspects endpoints and reads their deliveries;
Luge's server receives the webhooks.
luge-cli webhook list
luge-cli webhook create "GitHub CI" --to agent --target "General Agent" --source github
# prints the inbound_url + signing secret (secret is shown ONCE — save it)
luge-cli webhook create "Stripe" --to workflow --target "veille-techno" --source stripe
luge-cli webhook create "Orders" --to data_table --target "Orders" \
--map customer=data.customer.name --map amount=data.total # one row per POST
luge-cli webhook create "Debug" --to log_only # just log, triggers nothing
luge-cli webhook show <id|name>
luge-cli webhook deliveries <id|name> # what has arrived (the journal)
luge-cli webhook delivery <id|name> <delivery-id> # one delivery in full
luge-cli webhook update <id|name> --disable | --regenerate-secret
luge-cli webhook delete <id|name>
--to agent|workflow resolves --target (an agent or workflow, by id or name)
into the endpoint's destination. --to data_table resolves --target to a table
and maps the payload into a row: each --map <column>=<payload.path> fills a
column from a dot-path into the delivery body, and every required column must be
mapped. The signing secret is returned only on create and --regenerate-secret
— capture it then, it is not shown again.
Claude Code skill
The package bundles a Claude Code skill (luge-platform) that teaches an agent the
whole CLI — the card-work protocol (read the thread, announce, deliver, mark done
only when asked) plus the schedule / workflow / activity / channel / dm / agent /
artifact / webhook / settings surfaces and their non-obvious semantics. make install installs it; to (re)install it on its own:
luge-cli claude skill install --force # copies it to ~/.claude/skills/luge-platform
Unlike the editable CLI, the skill is a copy — it does not update on
git pull. Re-run luge-cli claude skill install --force after the bundled skill
changes.
Develop
make sync # create/refresh the dev environment
make hooks # install the pre-commit git hooks (once)
make check # lint (ruff) + type-check (mypy) + test (pytest) — what CI runs
make format # auto-fix lint issues and format
make test runs the suite (filters, resolution, HTTP client mocked, config, CLI
wiring). CI (GitHub Actions) runs lint, type-check, and test on every push and PR.
Release
make build # sdist + wheel (runs `check` first)
make publish # upload to PyPI via twine + ~/.pypirc (REPO=testpypi to test)
make release # tag vX.Y.Z + create a GitHub release from CHANGELOG.md
Bump version in pyproject.toml and add a CHANGELOG.md entry before releasing.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file luge_cli-0.28.0.tar.gz.
File metadata
- Download URL: luge_cli-0.28.0.tar.gz
- Upload date:
- Size: 189.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.11
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
0a86f9fbd11cae17215c5c1bf8b954236a25c759ef69f8f5212e794db27474b7
|
|
| MD5 |
f52072f38f902154786bcc49008b2486
|
|
| BLAKE2b-256 |
d22ad16aff80175b38c217bb44e8db265c1534de3426df54d2f68d02ba9601b3
|
File details
Details for the file luge_cli-0.28.0-py3-none-any.whl.
File metadata
- Download URL: luge_cli-0.28.0-py3-none-any.whl
- Upload date:
- Size: 127.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.11
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
4dedf717673174d0584cc1296aa31b8fd21dbd5fc82a5a2b1253be46c7f100b5
|
|
| MD5 |
25379ba2a355b3651c9ae3c345613480
|
|
| BLAKE2b-256 |
0ecb5154cb3f0680cccc0a31d574b298740b58a875a15f0562cfe4a499851df9
|