CLI
Edit collaborative text with the CLI
Read, check, edit, and save live text with version and hash expectations.
Edit the live document
Use the text API for Markdown and source files that are open collaboratively in CoCalc. It reads the live document, which can differ from the disk copy. Use the notebook, task, chat, or other document-specific API for structured documents.
Complete the quickstart first. This Bash recipe edits
an existing text file, replacing exactly one occurrence of Status: draft with
Status: reviewed. Use a scratch copy to learn the workflow.
Step 1: Select the project and inspect the API
Replace the project placeholder with the full ID from project list, and set
TEXT_PATH to the existing file's path inside that project:
export CLI_PROFILE=cocalc-ai
export PROJECT_ID='REPLACE_WITH_FULL_PROJECT_ID'
export TEXT_PATH='/home/user/notes.md'
cocalc --profile "$CLI_PROFILE" project get --project "$PROJECT_ID"
cocalc exec-api
Use the absolute path inside the remote project, adjusting /home/user if
needed. Relative text paths resolve against the CLI computer's home directory,
not the remote project's home.
Confirm the project before editing. exec-api describes the API bundled with
your installed CLI. JavaScript passed to cocalc exec runs in the CLI process;
its api.text methods access the selected project's collaborative state.
Step 2: Read, check, and replace one passage
The quoted heredoc keeps your shell from interpreting the JavaScript. The script reads its selection from the exported variables:
cocalc --profile "$CLI_PROFILE" --json exec --stdin <<'JS'
const projectIdentifier = process.env.PROJECT_ID;
const path = process.env.TEXT_PATH;
if (!projectIdentifier || !path) {
throw new Error("Set PROJECT_ID and TEXT_PATH first");
}
const doc = api.text.open({ projectIdentifier, path });
if (!doc.getAssociation().supportsTextApi) {
throw new Error("Use the document-specific API for this file type");
}
const before = await doc.read();
const oldText = "Status: draft";
const newText = "Status: reviewed";
const matches = before.text.split(oldText).length - 1;
if (matches !== 1) {
throw new Error(`Expected one matching passage; found ${matches}`);
}
const changed = await doc.replace(oldText, newText, {
expectedLatestVersionId: before.latestVersionId,
expectedHash: before.hash,
saveToDisk: true,
});
if (changed.replaceCount !== 1) {
throw new Error("The requested replacement was not confirmed");
}
return {
project_id: changed.project.project_id,
path: changed.path,
replaceCount: changed.replaceCount,
latestVersionId: changed.latestVersionId,
hash: changed.hash,
};
JS
Require successful command completion and ok:true. Inspect data.result for
the intended project/path and replaceCount:1. Script return values from
cocalc exec are nested under data.result, not directly under data.
Step 3: Read back and review
cocalc --profile "$CLI_PROFILE" --json exec --stdin <<'JS'
const projectIdentifier = process.env.PROJECT_ID;
const path = process.env.TEXT_PATH;
if (!projectIdentifier || !path) {
throw new Error("Set PROJECT_ID and TEXT_PATH first");
}
return await api.text.open({ projectIdentifier, path }).read();
JS
Review the returned text or open the file in CoCalc. If the edit command times out, read back before retrying: the edit may already have reached the document. The recipe deliberately refuses to replace zero or multiple matching passages.
Concurrency and saving
expectedLatestVersionId and expectedHash compare your read with the session's
current state before the edit. They are optimistic checks, not an exclusive
editing lock. On a mismatch, read the latest document and reconsider the edit;
do not remove the checks just to make the command succeed.
write replaces the text, append adds text, and replace changes a matching
passage. replace changes the first match unless all:true is supplied.
Writes save to disk by default. saveToDisk:false still changes and saves the
live collaborative state; it is not a dry run. A disk-save failure can occur
after the live edit, so review both states before attempting recovery.
The document facade returned by api.text.open() has no public close() method.
Its methods manage their session leases. Do not add a guessed doc.close() call.
For shell-process results and retry decisions, see scripting and results. For notebooks, use the live notebook workflow.