Constly IDE contract
Contract version: 1 — ships in Constly 4.7.0.
This is the public, stable way for another program — a VS Code extension, a JetBrains plugin, a shell script, a Makefile — to open a file in Constly and put the cursor somewhere useful. It is a command line. There is no daemon, no socket, no port, and nothing to configure.
Constly’s own copy of this document lives at docs/IDE-CONTRACT.md; the design
that produced it is docs/DESIGN-ide-extensions.md §2, and the grammar line
below is pinned byte-for-byte against it by docs/doc-anchors.tsv so the two
cannot drift apart quietly.
The command line
constly [--goto=<line>[:<col>]] [--from=<ide-id>[:<port>]] [--] <path>...
constly --version
| Element | Rule |
|---|---|
<path>... |
One or more paths; all are opened. A relative path is made absolute against the caller’s working directory — meaningful for a terminal user, and for a second launch, which carries the caller’s cwd to the running instance. Paths are made absolute, not canonical: the spelling you pass is the spelling Constly shows, recents remember, and the session file keeps. On macOS a cold start through open … --args has a working directory of /, so pass absolute paths there. |
--goto=L[:C] |
1-based line and optional column (UTF-16 code units). Applies to the first path. 0 means 1; a value past the end of the file lands on the last line; a malformed value is ignored. |
--from=id[:port] |
Who is handing the file over. id matches ^[a-z][a-z0-9-]{0,31}$ — for VS Code and its forks, the editor’s own URI scheme (vscode, vscode-insiders, vscodium, cursor, windsurf, …); jetbrains for the JetBrains IDEs, with an optional numeric REST port. It describes the hand-off, so unlike --goto it applies to every path in the launch. Recorded per tab and refreshed on every hand-off, including when the tab is already open. Malformed values are ignored. Constly does not act on it yet; the return trip (“Back to your IDE”) is what will. |
| Single-token flags | Prefer --flag=value. --goto L:C and --from id are accepted too, but only by Constly 4.7.0 and later — see “There is no version gate” below, which is the reason to send the = form. In that two-token form a value Constly cannot parse is still treated as the flag’s value while another path is on the line (--goto a.md b.md opens b.md alone — a.md is the value and never opens), and becomes a path itself only when nothing else would open (--goto a.md opens a.md with no caret): one more reason to send --flag=value. |
--version |
Prints Constly <version> to stdout and exits 0, without opening, focusing or flashing a window, and without disturbing a running instance. macOS and Linux; the Windows build is a GUI-subsystem binary with no console. For humans and scripts. |
Unknown - / -- flags |
Ignored. The contract is forward compatible by construction. |
-- |
Ends flag parsing, so a path may begin with -. |
Nothing is passed to a shell. Spawn Constly with an argument array.
Where to find the binary
| Platform | Path |
|---|---|
| macOS | /Applications/Constly.app/Contents/MacOS/constly (the binary inside the bundle) |
| Windows | constly.exe in the install location |
| Linux | constly on PATH (deb/rpm), or the binary from the tar |
On macOS, running that inner binary reaches an already-running Constly through
its single-instance channel, flags included. open -a Constly.app does not:
macOS drops --args when the app is already running.
There is no version gate
Do not probe for a version. Do not read the registry, dpkg, rpm or
Info.plist, and do not call --version to decide what to send. Send the flags
unconditionally: a Constly older than 4.7.0 skips any argument beginning with
- and opens the file anyway, so the only observable difference on an old
install is that the cursor stays where it was.
One thing does differ before 4.7.0: older builds open only the first path of a launch. Send one launch per path, sequentially, and a multi-file hand-off behaves identically on both.
Opening one file twice, on purpose
macOS has two ways to hand a file to an application, and a cold start from an IDE uses both at once:
open -a /Applications/Constly.app /abs/notes.md --args --goto=3:1 -- /abs/notes.md
The document arrives through LaunchServices (no caret) and the argument list arrives as argv (with the caret), in whichever order the system chooses. Constly folds the two into one tab and applies the caret from whichever arrival carried it. You do not need to work around this; the duplicate is the supported way to get a caret on a cold start.
What Constly does with the file
- If a window already shows that file, that window comes forward and its tab is surfaced — the buffer, its unsaved edits and its undo history are untouched. Only the caret moves.
- Otherwise the file opens where the user’s New/Open preference says: its own window (the default) or a new tab.
- An open from a command line is treated as the user gesture it is, and grants Constly access to that file exactly like a double-click in Finder or Explorer.
Saving: the filesystem is the channel
Constly does not autosave to disk. Unsaved work is protected by Constly’s own session drafts, which survive a quit, a crash and an update — but they are not the file. Your IDE sees Constly’s edits when the user presses ⌘S / Ctrl+S in Constly, or uses Back to your IDE, which saves first.
Tell your users this plainly. It is the single thing that most often reads as “the bridge is broken” when it is working exactly as designed.
The other direction is symmetrical, and it is the caller’s job:
- Save the file before handing it over. If the buffer is dirty in your IDE, save it first; if it is untitled, ask the user to save it somewhere first; if the save fails or is cancelled, skip that file rather than launching Constly on stale bytes.
- Never write the file from the bridge itself. Save what the user typed; nothing else.
When the file changes on disk under Constly, a clean tab either reloads silently (if the user has opted into that) or offers a Reload / Always / Dismiss banner; a tab with unsaved edits always gets the banner, and a save that would overwrite an external change asks first. If the file is moved or deleted, the tab says so and the next save asks where to put the bytes instead of silently recreating the file — Constly never detaches a tab from its file on its own.
Encoding, line endings and byte-order marks are preserved: Constly rewrites only the ranges that changed, so a round trip through it does not reformat a file.
Worked examples
# Open a file at line 42, column 9, from VS Code.
constly --goto=42:9 --from=vscode -- /home/ada/notes/plan.md
# Open a file at line 42, from IntelliJ, telling Constly the REST port.
constly --goto=42 --from=jetbrains:63342 -- /home/ada/notes/plan.md
# Three files: three launches, in order.
constly --from=cursor -- /abs/a.md
constly --from=cursor -- /abs/b.md
constly --from=cursor -- /abs/c.md
# macOS, nothing running yet: document + argv, one tab, caret on line 3.
open -a /Applications/Constly.app /abs/a.md --args --goto=3:1 -- /abs/a.md
# macOS, already running: use the inner binary, which reaches it.
/Applications/Constly.app/Contents/MacOS/constly --goto=3:1 -- /abs/a.md
Not in version 1
- A
constly://URL scheme. It is a documented attack surface and is deferred; if it ships it will be confirmation-gated and will not change this grammar. - Any network protocol. Constly’s bridge makes no network calls, and neither should yours.
- Reading anything back out of Constly. The file on disk is the whole interface.