Skip to main content

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

CI PyPI Python

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 columnslink, 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, aggcount|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 chatluge-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 messageluge-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 chatluge-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.

Colleaguesluge-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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

luge_cli-0.28.0.tar.gz (189.6 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

luge_cli-0.28.0-py3-none-any.whl (127.4 kB view details)

Uploaded Python 3

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

Hashes for luge_cli-0.28.0.tar.gz
Algorithm Hash digest
SHA256 0a86f9fbd11cae17215c5c1bf8b954236a25c759ef69f8f5212e794db27474b7
MD5 f52072f38f902154786bcc49008b2486
BLAKE2b-256 d22ad16aff80175b38c217bb44e8db265c1534de3426df54d2f68d02ba9601b3

See more details on using hashes here.

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

Hashes for luge_cli-0.28.0-py3-none-any.whl
Algorithm Hash digest
SHA256 4dedf717673174d0584cc1296aa31b8fd21dbd5fc82a5a2b1253be46c7f100b5
MD5 25379ba2a355b3651c9ae3c345613480
BLAKE2b-256 0ecb5154cb3f0680cccc0a31d574b298740b58a875a15f0562cfe4a499851df9

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page