| name | octo-drive |
| version | 0.1.0 |
| description | Octo Drive โ spaces, folders, file upload/download, online-document mounts, share links, invites, IM-attachment transfer. Works with a bot token or a user API key; the CLI routes by token kind. Load after octo-shared. |
| metadata | {"requires":{"bins":["octo-cli"],"skills":["octo-shared"]}} |
octo-drive โ network drive operations
45 commands over one backend. Everything is scoped to a drive space: either your personal space or a shared one you are a member of.
1. Credentials โ nothing drive-specific
Drive uses the same credential as every other domain. Set one of:
export OCTO_TOKEN=<uk_โฆ | bf_โฆ | app_โฆ>
export OCTO_BOT_TOKEN=<bf_โฆ | app_โฆ>
OCTO_TOKEN wins when both are set, so you can run one command as a real person without disturbing a bot setup:
OCTO_TOKEN=$UK_KEY octo-cli drive space list
A stored profile (octo-cli auth login) takes precedence over both. Do not create a drive-only profile โ there is no such concept.
The token kind decides which identity the backend sees, and the CLI routes the request accordingly:
| token | acts as | mount |
|---|
uk_โฆ | the real person who owns the key | /v1/user/drive/* |
bf_โฆ | the User Bot | /v1/bot/drive/* |
app_โฆ | the App Bot | /v1/bot/drive/* |
Any other credential fails locally with TOKEN_KIND_NOT_ALLOWED (exit 2) โ switch credentials, don't retry.
A bot has no implicit access: to touch a shared space it must be added as a member, exactly like a person. If a bot gets permission_denied, add it with drive member add. An app_โฆ token may also lack a resolvable space entirely; that surfaces as a 401 (exit 3) from the server, and the fix is a bf_โฆ or uk_โฆ credential.
Drive never sends X-Space-Id โ the tenant comes from the verified identity.
2. Ids โ copy them, never compute with them
| id | shape | produced by | consumed by |
|---|
| drive space id | opaque string, e.g. personal:<octo-space>:<uid> or shared:<uuid> | space create/list/get/ensure-personal โ .data.id | --space-id, --target-space-id, positional <space-id> |
| drive file id | decimal string | browse, folder create, upload file, doc mount, im-transfer create โ .data.id | <file-id>, --parent-id ("0" = space root) |
doc_id / doc_space_id | opaque strings | doc candidates, doc list, file get | document links only |
share_id vs share_token | opaque strings | share create, share blob-create, share list | share_id โ share revoke; the token is embedded in share_url |
invite_id vs invite_token | opaque strings | invite create, invite list | invite_id โ invite revoke; invite_token โ invite accept |
File ids are uint64 and are emitted as JSON strings on purpose. Values above 2^53 would be silently rounded by a JavaScript-style parser, addressing a different file. So: pass them through verbatim (-q '.data.id' | tr -d '"'), never do arithmetic on them, never reformat them. The CLI rejects a non-decimal or out-of-range id locally.
Why | tr -d '"' on every capture below. --jq/-q prints a JSON value, and ids are JSON strings โ so -q '.data.id' emits "123" including the quotes, and a quoted id is rejected by the next command's id validation. There is no raw-output flag; -q is the only output filter the CLI defines. Strip the quotes in the shell. (Numbers already come out bare, so this matters for string-valued fields: every id, share_url, and every opaque token.)
Two traps worth naming:
space_id (the drive space) and doc_space_id (the document's own Octo Space) are different scopes. Building a document link from space_id produces a link to the wrong place. The CLI fails closed rather than substituting.
doc unmount takes the mount node id (.data.id), not the doc_id.
3. The five workflows
Space โ folder โ upload โ share โ download
SPACE=$(octo-cli drive space create --name "Project files" -q '.data.id' | tr -d '"')
FOLDER=$(octo-cli drive folder create --space-id "$SPACE" --parent-id 0 --name Contracts -q '.data.id' | tr -d '"')
FILE=$(octo-cli drive upload file ./contract.pdf --space-id "$SPACE" --parent-id "$FOLDER" -q '.data.id' | tr -d '"')
SHARE_URL=$(octo-cli drive share create "$FILE" -q '.data.share_url' | tr -d '"')
octo-cli drive share access "$SHARE_URL"
octo-cli drive share download "$SHARE_URL" -o ./contract.pdf
octo-cli drive download file "$FILE" -o ./copy.pdf
Personal space โ mount a document โ share its link
SPACE=$(octo-cli drive space ensure-personal -q '.data.id' | tr -d '"')
DOC=$(octo-cli drive doc candidates --space-id "$SPACE" -q '.data.items[0].doc_id' | tr -d '"')
MOUNT=$(octo-cli drive doc mount --space-id "$SPACE" --doc-id "$DOC" -q '.data.id' | tr -d '"')
DOC_URL=$(octo-cli drive share create "$MOUNT" -q '.data.share_url' | tr -d '"')
octo-cli drive share access "$DOC_URL"
doc mount takes no --doc-title: the title and the document's real Octo Space are read server-side from the document metadata, so they cannot drift.
Invite a member
SPACE=$(octo-cli drive space create --name "Collab" -q '.data.id' | tr -d '"')
INVITE_ID=$(octo-cli drive invite create "$SPACE" --role editor -q '.data.invite_id' | tr -d '"')
TOKEN=$(octo-cli drive invite list "$SPACE" -q '.data.invites[0].invite_token' | tr -d '"')
octo-cli drive invite accept "$TOKEN"
octo-cli drive invite revoke "$SPACE" "$INVITE_ID"
invite_id / invite_token are base64url and may start with -; prefer
--invite-id / --invite-token in scripts so a leading dash is never parsed as a flag.
Roles accepted by invite create: preview_only, downloader, uploader_downloader, editor, admin (admin only if you are the space's super_admin). custom and super_admin are rejected on invites. Or add a known uid directly:
octo-cli drive member add "$SPACE" --uid "$UID" --role editor
member add / member set-role accept one more role than invites do โ custom, the lowest rank (below preview_only). super_admin is never grantable: it is bound to the space creator at space creation.
| role | member add / set-role | invite create |
|---|
preview_only / downloader / uploader_downloader / editor | โ
| โ
|
admin | โ
super_admin only | โ
super_admin only |
custom | โ
| โ |
super_admin | โ | โ |
Drive has no user search โ get a uid from the message/group commands or your own context.
IM attachment โ drive
MSG=$(octo-cli message search files --chat-id "$GROUP" -q '.data.items[0].message_id' | tr -d '"')
FILE=$(octo-cli drive im-transfer create \
--im-group-no "$GROUP" --im-channel-type 2 --im-msg-id "$MSG" \
--target-space-id "$SPACE" -q '.data.id' | tr -d '"')
--im-channel-type is required: 1=DM, 2=group, 5=thread, and it must be the kind the message actually came from. It picks the upstream message-read route (1 uses the DM route; 2 and 5 share the group route, where group vs sub-thread comes from the composite group_no), and it is stored as the first segment of the row's source_key (channelType#channelID#msgID), which the chat file-card's already-transferred lookup matches on โ a wrong value makes that lookup miss. Anything outside 1|2|5 is rejected locally (ENUM_NOT_ALLOWED, exit 2).
Transfer idempotency is keyed on (target space, type=blob, object path), not on the channel type: a replay of the same message returns the existing row with idempotent: true, and a wrong channel type cannot produce a duplicate file. Keep the message id a string.
Browse and act
octo-cli drive browse --space-id "$SPACE" --parent-id 0
octo-cli drive browse --space-id "$SPACE" --type blob --source user-upload
FILE=$(octo-cli drive browse --space-id "$SPACE" -q '.data.entries[0].id' | tr -d '"')
octo-cli drive file get "$FILE"
octo-cli drive file move "$FILE" --parent-id "$FOLDER"
octo-cli drive file rename "$FILE" --name new-name.pdf
octo-cli drive file copy "$FILE" --parent-id "$FOLDER"
file get is how you branch: type tells you whether an id is a blob, a mounted document, or a folder.
browse returns the complete listing; its page object is an envelope, not a database page, so --page-index / --page-size do not actually narrow the result yet.
Full-text search
drive search is full-text search across every space the caller can see (name + blob/doc body via OpenSearch), scoped by the caller's own membership โ a bot only sees spaces it was added to. --page-index here IS a real page (0-based).
octo-cli drive search --q "ๆฅ้ๅ"
octo-cli drive search --q "้ข็ฎ" --scope space --space-id "$SPACE"
octo-cli drive search --q "ๅๅ" --data '{"filters":{"type":"blob","owner_scope":"me"}}'
Advanced --filters (pass via --data): type (folder|doc|blob), owner_scope (me|others), updated_after (RFC3339), size_min/size_max (bytes). Each hit carries file_id, path (breadcrumb), type, highlights.{name,body} with <mark> fragments, and owner_uid/updater_uid (resolve display names via the message/group commands โ search returns uids, not names). Requires the server's search backend to be configured; otherwise the call returns unavailable (503).
4. Upload and download in detail
drive upload file runs prepare โ PUT to object storage โ confirm. The PUT goes out on a separate HTTP client that carries no Octo credential โ the presigned URL is its own authorisation. If anything fails after the pending row exists, the CLI cancels it and the error reports the file_id plus the cancel outcome:
{"ok":false,"error":{"code":"UPLOAD_FAILED","detail":{"file_id":"42","pending_file":"cancelled"}}}
If pending_file is not cancelled, run octo-cli drive upload cancel <file-id> yourself.
drive download file and drive share download write to a randomly-named partial file next to the destination, fsync, then rename โ an interrupted transfer never leaves a truncated file, and a pre-created symlink at a guessable path cannot redirect the write. An existing destination is refused unless you pass --overwrite. The result carries a sha256 you can verify.
The low-level steps stay available (upload prepare|confirm|cancel, download url) if you need to drive the transfer yourself.
drive blob create is a different thing and is rarely what you want: it registers an object already in storage rather than uploading one. The backend verifies it โ an --object-path storage does not hold is invalid_argument, and a --size that conflicts with the stored object is rejected (--size 0 for a non-empty object included) โ so it can no longer produce a row that lists fine and 404s on download. If storage is unreachable the probe is inconclusive and you get a 500 to retry, not invalid_argument. It still persists no download URL, so share download on such a row answers not_found. Use drive upload file unless the bytes are already in the bucket.
5. Share links
There is exactly one thing the two sides exchange: data.share_url.
octo-cli drive share create "$FILE" --permission download --expires-in-seconds 86400 --password-file ./pw
- The password is passed out of band โ it is never in the URL, and it is masked in
--verbose / --dry-run output.
- Prefer
--password-file <path> (or --password-file - for stdin) over --password. A value on argv is readable from ps and /proc and lands in shell history for the process lifetime, the same reason auth login takes its token from --token-file / stdin and never from the command line. --password still works for interactive use. One trailing newline is stripped from the file; nothing else is, so a password may begin or end with a space.
--permission is held to the spec's view | download enum locally: anything else is ENUM_NOT_ALLOWED / exit 2 with no request sent.
- Both sides need a credential. There is no anonymous share. The token (and password) authorise the share; your credential authenticates you. The receiver does not have to be a member of the file's space.
share access / share download accept only links on your configured Octo origin โ same scheme, host and port โ in exactly the /drive/s/<token> or /d/<docId>?sp=<docSpaceId> shape. Anything else fails with INVALID_SHARE_URL โ the CLI parses the link, it never fetches the host in it. Pass the link exactly as share create produced it; a scheme-downgraded copy of an otherwise correct link is refused.
downloadable tells you which command to use next. Documents are always false, and so is a blob shared with --permission view โ share download on either answers permission_denied. Only --permission download yields bytes.
- Revoke with the
share_id, not the token: octo-cli drive share revoke "$SHARE_ID". The share_id is itself treated as a secret โ the backend returns one opaque id that is both the management handle and the access token โ so --verbose, --dry-run and error output show it as ***REDACTED***. That is the mask, not a wrong id: the real value still goes on the wire. Read it from share create / output, which is not masked.
6. Errors
| code | type / exit | what to do |
|---|
TOKEN_KIND_NOT_ALLOWED | validation / 2 | switch credentials; do not retry |
ENUM_NOT_ALLOWED | validation / 2 | the value is outside the spec's enum; the hint lists the accepted set |
unauthorized | auth / 3 | token invalid, revoked, or the user/bot is inactive |
permission_denied | permission / 1 | the identity lacks the space role; drive member add it |
password_required / wrong_password | permission / 1 | pass or fix the password (--password-file) |
share_expired | permission / 1 | ask for a new link |
not_found | api_error / 1 | check the id and that the space is reachable |
conflict | validation / 2 | re-read state, then retry |
invalid_argument | validation / 2 | check the schema: octo-cli schema drive.<op> |
FILE_EXISTS | validation / 2 | pass --overwrite or pick another path |
NOT_DOWNLOADABLE | validation / 2 | a document link; use share access or a browser |
INVALID_SHARE_URL | validation / 2 | pass the share_url exactly as produced |
MISSING_DOC_SPACE_ID | validation / 2 | re-mount the document; never substitute the drive space id |
UNSAFE_PRESIGNED_URL | api_error / 1 | the backend returned an unusable URL, or one whose host resolves to this machine; report it |
TRANSFER_REDIRECTED_LOCALLY | validation / 2 | the transfer was rerouted to this machine but the URL does not name it; check / / / |
7. Destructive commands
space delete, member remove, folder delete, blob delete, doc unmount, share revoke, invite revoke are all high-risk writes with no confirmation prompt (agent runtimes cannot prompt). Deletes are soft on the backend, but folder delete takes the whole subtree with it.
Preview any write first:
octo-cli drive folder delete "$FOLDER" --dry-run
--dry-run on upload file, download file, share create and share download describes the plan and stops: no pending row is created, no URL is fetched, nothing is written to disk.
8. Full command list
drive browse
drive search
drive space create | list | ensure-personal | get | rename | delete
drive member list | add | set-role | remove
drive folder create | list | rename | move | delete
drive file get | move | copy | rename
drive blob create | get | list | delete
drive upload file | prepare | confirm | cancel
drive download file | url
drive doc mount | unmount | list | candidates
drive share create | blob-create | list | revoke | access | download
drive invite create | list | revoke | accept
drive im-transfer create
Per-command flags: octo-cli drive <group> <verb> --help, or octo-cli schema drive.<group>.<verb> for the wire contract.