Skip to content

File System ​

OpenViking provides Unix-like file system operations for managing context.

For reading and writing file contents, see Content. WebDAV provides file access through WebDAV clients.

API Reference ​

ls() ​

List directory contents.

Parameters

ParameterTypeRequiredDefaultDescription
uristrYes-Viking URI
simpleboolNoFalseReturn only relative paths
recursiveboolNoFalseList all subdirectories recursively
outputstrNoHTTP: agent; SDKs: originalOutput format: agent or original
abs_limitintNo256Maximum returned abstract length
include_abstractboolNoUnsetInclude directory L0 abstracts. When unset, follows the legacy output behavior (agent: included; original: omitted)
include_overviewboolNoUnsetInclude directory L1 overviews. Unset means omitted
overview_limitintNo4000Maximum returned overview length
show_all_hiddenboolNoFalseInclude hidden files like -a
node_limitintNo1000Maximum number of results
offsetintNo0Number of visible results to skip
limitintNoNoneAlias for node_limit
sort_bystrNoNoneSort directories and files within their groups by name or mtime before pagination; directories remain first
sort_orderstrNoascSort direction: asc or desc
extra_fieldslist[str]NoNoneExtra fields to include: locked, id, count
include_tagsboolNoFalseReturn tags without requiring a tag filter
tagsstring[]NoUnsetReturn only entries matching every supplied k=v retrieval tag

tags uses AND semantics and is applied before offset and limit. L0/L1 content is attached only to the selected page of directory entries and does not consume node_limit. An explicit include_abstract=true|false overrides the legacy behavior implied by output. Tags are included for filtered responses; for an unfiltered response, request include_tags=true (CLI: -f tags).

Entry Structure

python
{
    "name": "docs",           # File/directory name
    "size": 4096,             # Size in bytes
    "mode": 16877,            # File mode
    "modTime": "2024-01-01T00:00:00Z",  # ISO timestamp
    "isDir": True,            # True if directory
    "uri": "viking://resources/docs/",  # Viking URI
    "meta": {}                # Optional metadata
}

If the caller can read the parent directory but cannot read one of its direct children, ls still returns a name placeholder without size, modification time, abstract, or storage metadata:

python
{
    "name": "restricted",
    "isDir": True,
    "uri": "viking://resources/restricted",
    "access": "denied"
}

Content operations such as stat and read return HTTP 403 PermissionDenied for that URI. Recursive listing retains the inaccessible directory itself but does not descend into it, and search results omit unreadable content. This discoverable-name behavior applies only to the shared viking://resources namespace; private user and peer namespaces retain their existing hiding rules.

On case-insensitive localfs, directory name aliases that differ only by case may affect what ls, tree, and glob can see. For example, if the real directory is viking://resources/bob-private/, a request for viking://resources/BOB-PRIVATE/ may list the same directory; on case-sensitive systems, the alias directory returns NOT_FOUND when it does not exist.

Python SDK (HTTP)

python
page = client.ls_page(
    uri="viking://resources/",
    offset=100,
    limit=100,
    sort_by="mtime",
    sort_order="desc",
    include_abstract=True,
    include_overview=True,
)
for entry in page["result"]:
    type_str = "dir" if entry['isDir'] else "file"
    print(f"{entry['name']} - {type_str}")
print("has more nodes:", page["has_more"])

TypeScript SDK

typescript
const page = await client.listPage("viking://resources/docs/", {
  includeAbstract: true,
  includeOverview: true,
});
console.log(page.result);
console.log("has more nodes:", page.hasMore);

Go SDK

go
page, err := client.ListPage(ctx, "viking://resources/", &openviking.ListOptions{
    IncludeAbstract: openviking.Bool(true),
    AbsLimit:        512,
    IncludeOverview: openviking.Bool(true),
    OverviewLimit:   4000,
})
if err != nil {
    return err
}
for _, entry := range page.Result {
    fmt.Println(entry)
}
fmt.Println("has more nodes:", page.HasMore)

When only the entry array is needed, use the compatibility methods ls in Python, list in TypeScript, and List in Go. To detect whether limit or node_limit truncated the result, use ls_page, listPage, and ListPage, respectively.

HTTP API

GET /api/v1/fs/ls?uri={uri}&offset={int}&limit={int}
bash
# Basic listing
curl -X GET "http://localhost:1933/api/v1/fs/ls?uri=viking://resources/" \
  -H "X-API-Key: your-key"

# Simple path list
curl -X GET "http://localhost:1933/api/v1/fs/ls?uri=viking://resources/&simple=true" \
  -H "X-API-Key: your-key"

# Recursive listing
curl -X GET "http://localhost:1933/api/v1/fs/ls?uri=viking://resources/&recursive=true" \
  -H "X-API-Key: your-key"

CLI

bash
ov ls viking://resources/ [--simple] [--recursive] [--include-abstract[=true|false]] [--include-overview[=true|false]] [--tags team=search,env=prod] [-f FIELDS]
ov tree viking://resources/my-project/ [--simple] [--tags team=search,env=prod] [-f FIELDS]
ov glob "**/*.md" [--uri viking://resources/] [--simple] [--tags team=search,env=prod] [-f FIELDS]

-f/--fields accepts a comma-separated list of columns to display (ps -o style), producing a column-aligned table with a header row. Available fields: name, uri, path, type, size, mode, mtime, locked, id, count, abstract, overview, tags. Combining --simple with -f outputs comma-separated values (no header, no tree indentation), one entry per line — suitable for scripting pipelines. When --simple is used without -f, the previous behavior (bare URI per line) is preserved.

The HTTP result remains an entry array. has_more=true means more matching nodes remain after visibility, tags, offset, and limit are applied. The Python, TypeScript, and Go SDKs continue to return the result array. When more nodes are available, the CLI appends a pagination hint to its output.

Response (output=original)

json
{
  "status": "ok",
  "result": [
    {
      "name": "docs",
      "size": 4096,
      "mode": 16877,
      "modTime": "2024-01-01T00:00:00Z",
      "isDir": true,
      "uri": "viking://resources/docs/"
    }
  ],
  "has_more": true
}

tree() ​

Get directory tree structure.

Parameters

ParameterTypeRequiredDefaultDescription
uristrYes-Viking URI
outputstrNoHTTP: agent; SDKs: originalOutput format: agent or original
abs_limitintNoHTTP: 256; SDKs: 128Maximum returned abstract length
include_abstractboolNoUnsetInclude directory L0 abstracts. When unset, follows the legacy output behavior (agent: included; original: omitted)
include_overviewboolNoUnsetInclude directory L1 overviews. Unset means omitted
overview_limitintNo4000Maximum returned overview length
show_all_hiddenboolNoFalseInclude hidden files like -a
directories_onlyboolNoFalseReturn directory nodes only
node_limitintNo1000Maximum number of results
offsetintNo0Number of visible results to skip
limitintNoNoneAlias for node_limit
level_limitintNo3Maximum directory depth to traverse
extra_fieldslist[str]NoNoneExtra fields to include: locked, id, count
include_tagsboolNoFalseReturn tags without requiring a tag filter
tagsstring[]NoUnsetRetain only nodes matching every supplied k=v retrieval tag

Directory filtering and tags are applied before offset and limit. Abstracts and overviews are attached to the selected directory nodes and do not count toward node_limit. Explicit include_abstract=true|false overrides the legacy behavior implied by output. Tags are included for filtered responses; for an unfiltered response, request include_tags=true (CLI: -f tags).

Python SDK (HTTP)

python
page = client.tree_page(
    uri="viking://resources/",
    offset=100,
    limit=100,
    include_abstract=True,
    include_overview=True,
    directories_only=True,
)
for entry in page["result"]:
    type_str = "dir" if entry['isDir'] else "file"
    print(f"{entry['rel_path']} - {type_str}")
print("has more nodes:", page["has_more"])

TypeScript SDK

typescript
const page = await client.treePage("viking://resources/docs/", {
  nodeLimit: 100,
  includeAbstract: true,
  includeOverview: true,
});
console.log(page.result);
console.log("has more nodes:", page.hasMore);

Go SDK

go
page, err := client.TreePage(ctx, "viking://resources/", &openviking.TreeOptions{
    DirectoriesOnly: true,
    IncludeAbstract: openviking.Bool(true),
    IncludeOverview: openviking.Bool(true),
})
if err != nil {
    return err
}
for _, entry := range page.Result {
    fmt.Println(entry["rel_path"], entry["isDir"])
}
fmt.Println("has more nodes:", page.HasMore)

When only the node array is needed, use the compatibility method tree in Python and TypeScript or Tree in Go. To detect whether limit or node_limit truncated the result, use tree_page, treePage, and TreePage, respectively.

HTTP API

GET /api/v1/fs/tree?uri={uri}&offset={int}&limit={int}
bash
curl -X GET "http://localhost:1933/api/v1/fs/tree?uri=viking://resources/" \
  -H "X-API-Key: your-key"

CLI

bash
ov tree viking://resources/my-project/ \
  --directories-only --include-abstract --include-overview

As with ls, the HTTP result remains a node array and has_more is returned at the top level. When has_more=true, the CLI appends a pagination hint to the tree output.

Response (output=original)

json
{
  "status": "ok",
  "result": [
    {
      "name": "docs",
      "size": 4096,
      "isDir": true,
      "rel_path": "docs/",
      "uri": "viking://resources/docs/"
    },
    {
      "name": "api.md",
      "size": 1024,
      "isDir": false,
      "rel_path": "docs/api.md",
      "uri": "viking://resources/docs/api.md"
    }
  ],
  "has_more": true
}

stat() ​

Get file or directory status information. For directories, returns the count of items under the directory.

Parameters

ParameterTypeRequiredDefaultDescription
uristrYes-Viking URI (e.g. viking://resources/docs/api.md) or a 32-character hex vector record id

Python SDK (HTTP)

python
info = client.stat(uri="viking://resources/docs/api.md")
print(f"Size: {info['size']}")
print(f"Is directory: {info['isDir']}")

# For directories, returns item count
dir_info = client.stat(uri="viking://resources/docs")
if dir_info.get('isDir'):
    print(f"Item count: {dir_info.get('count')}")

TypeScript SDK

typescript
const metadata = await client.stat("viking://resources/docs/api.md");
console.log(metadata);

Go SDK

go
info, err := client.Stat(ctx, "viking://resources/docs/api.md")
if err != nil {
    return err
}
fmt.Println(info["size"], info["isDir"])

HTTP API

GET /api/v1/fs/stat?uri={uri}
bash
curl -X GET "http://localhost:1933/api/v1/fs/stat?uri=viking://resources/docs/api.md" \
  -H "X-API-Key: your-key"

CLI

bash
ov stat viking://resources/my-project/docs/api.md
ov stat viking://resources/my-project/docs

Response (File)

json
{
  "status": "ok",
  "result": {
    "name": "api.md",
    "size": 1024,
    "mode": 33188,
    "modTime": "2024-01-01T00:00:00Z",
    "isDir": false,
    "isLocked": false,
    "id": "a1b2c3d4e5f678901234567890abcdef",
    "uri": "viking://resources/docs/api.md"
  }
}

Response (Directory)

json
{
  "status": "ok",
  "result": {
    "name": "docs",
    "size": 4096,
    "mode": 16877,
    "modTime": "2024-01-01T00:00:00Z",
    "isDir": true,
    "isLocked": false,
    "uri": "viking://resources/docs",
    "count": 42
  }
}

The isLocked field reports whether the path is currently held by a path lock: the path itself has a valid lock (including an exact-path lock for the target), or any ancestor directory holds a TreeLock. It returns false when the LockManager is unavailable or the lookup fails. This is an advisory check, not a lock reservation: another writer may acquire a lock after the check, so writes must still handle ResourceBusyError.

The id field (files only) is the deterministic vector record primary key in VikingDB, computed as md5(f"{account_id}:{uri}") for level 2 (regular file) records. This value matches the id field in the vector collection schema and can be used to cross-reference vector records without an additional lookup. The field is omitted for directories because a directory may have multiple vector records across semantic levels (L0 abstract, L1 overview). Because indexing is asynchronous, a newly returned ID might not be resolvable immediately; lookup by ID can also fail after its vector record is deleted. In either case, stat(id) returns NOT_FOUND with a reason indicating that the data may not have been indexed yet or may have been deleted.

The count field (directories only) contains the estimated number of items (files and subdirectories) under this directory (from vector index).

On case-insensitive localfs, stat may also return metadata for a real file or directory through a case-only alias. For example, viking://resources/docs/REPORT.md may hit the real viking://resources/docs/report.md; on case-sensitive systems, the alias target returns NOT_FOUND when it does not exist.


attrs() ​

Get logical extended attributes for a file or directory.

Parameters

ParameterTypeRequiredDefaultDescription
uristrYes-Viking URI

Python SDK (HTTP)

python
attrs = client.attrs(uri="viking://resources/docs/api.md")
print(attrs["attrs"]["tags"])

TypeScript SDK

typescript
const attributes = await client.attrs("viking://resources/docs/api.md");
console.log(attributes);

Go SDK

go
attrs, err := client.Attrs(ctx, "viking://resources/docs/api.md")
if err != nil {
    return err
}
metadata := attrs["attrs"].(map[string]any)
fmt.Println(metadata["tags"])

HTTP API

GET /api/v1/fs/attrs?uri={uri}
POST /api/v1/fs/attrs/set_tags
bash
curl -X GET "http://localhost:1933/api/v1/fs/attrs?uri=viking://resources/docs/api.md" \
  -H "X-API-Key: your-key"

curl -X POST "http://localhost:1933/api/v1/fs/attrs/set_tags" \
  -H "X-API-Key: your-key" \
  -H "Content-Type: application/json" \
  -d '{"uri":"viking://resources/docs","tags":["team=search"],"mode":"append","recursive":true}'

CLI

bash
ov attrs get viking://resources/docs/api.md
ov attrs get viking://resources/docs/api.md tags
ov attrs get viking://user/alice/memories/experiences/foo.md memory.resource_refs
ov attrs set-tags viking://resources/docs/api.md --tags team=search,env=prod
ov attrs set-tags viking://resources/docs --tags team=search --mode append --recursive

For set-tags, directory targets update the directory semantic records; recursive=true also updates existing descendant files and directory semantic records.

Response (Resource)

json
{
  "status": "ok",
  "result": {
    "uri": "viking://resources/docs/api.md",
    "context_type": "resource",
    "attrs": {
      "tags": ["team=search", "env=prod"]
    }
  }
}

Response (Memory)

json
{
  "status": "ok",
  "result": {
    "uri": "viking://user/alice/memories/experiences/foo.md",
    "context_type": "memory",
    "attrs": {
      "memory": {
        "memory_type": "experiences",
        "name": "foo",
        "tags": ["ui"],
        "resource_refs": ["viking://resources/docs/api.md"]
      },
      "tags": ["team=search"]
    }
  }
}

attrs.memory is parsed from MEMORY_FIELDS metadata with content removed. attrs.tags is the explicit retrieval tag list used by attrs set-tags and search filters.


mkdir() ​

Create a directory.

Parameters

ParameterTypeRequiredDefaultDescription
uristrYes-Viking URI for the new directory
descriptionstrNonullInitial directory description. When omitted, the directory name is used as the default L0; when provided, this description is used. Both forms write .abstract.md and queue L0 vectorization.

Python SDK (HTTP)

python
client.mkdir(uri="viking://resources/new-project/")
client.mkdir(uri="viking://resources/new-project/", description="API docs directory")

TypeScript SDK

typescript
await client.mkdir("viking://resources/docs/guides/", "Project guides");

Go SDK

go
if err := client.Mkdir(ctx, "viking://resources/new-project/", "API docs directory"); err != nil {
    return err
}

HTTP API

POST /api/v1/fs/mkdir
bash
curl -X POST http://localhost:1933/api/v1/fs/mkdir \
  -H "Content-Type: application/json" \
  -H "X-API-Key: your-key" \
  -d '{
    "uri": "viking://resources/new-project/",
    "description": "API docs directory"
  }'

CLI

bash
ov mkdir viking://resources/new-project/
ov mkdir viking://resources/new-project/ --description "API docs directory"

Response

json
{
  "status": "ok",
  "result": {
    "uri": "viking://resources/new-project/"
  }
}

On case-insensitive localfs, creating a directory that differs only by case may hit an existing directory. For example, if viking://resources/docs/ already exists, ov mkdir viking://resources/DOCS/ may act on the same underlying directory; on case-sensitive systems, they are separate paths.


rm() ​

Remove file or directory. When removing directories recursively, returns the estimated number of items deleted.

rm is idempotent: removing a valid URI that does not exist still succeeds. Invalid URI formats, unsupported schemes, and internal scopes such as temp or queue return INVALID_URI.

Parameters

ParameterTypeRequiredDefaultDescription
uristrYes-Viking URI to remove
waitboolNoFalseWait for the semantic refresh after deletion
recursiveboolNoFalseRemove directory recursively

Python SDK (HTTP)

python
# Remove single file
client.rm(uri="viking://resources/docs/old.md")

# Remove directory recursively
client.rm(uri="viking://resources/old-project/", recursive=True)

TypeScript SDK

typescript
await client.remove("viking://resources/docs/old.md");

Go SDK

go
err := client.Remove(ctx, "viking://resources/old-project/", &openviking.RemoveOptions{
    Recursive: true,
})
if err != nil {
    return err
}

HTTP API

DELETE /api/v1/fs?uri={uri}&recursive={bool}
bash
# Remove single file
curl -X DELETE "http://localhost:1933/api/v1/fs?uri=viking://resources/docs/old.md" \
  -H "X-API-Key: your-key"

# Remove directory recursively
curl -X DELETE "http://localhost:1933/api/v1/fs?uri=viking://resources/old-project/&recursive=true" \
  -H "X-API-Key: your-key"

CLI

bash
ov rm viking://resources/old.md [--recursive]

Response (Single file)

json
{
  "status": "ok",
  "result": {
    "uri": "viking://resources/docs/old.md"
  }
}

Response (Recursive delete)

json
{
  "status": "ok",
  "result": {
    "uri": "viking://resources/old-project/",
    "estimated_deleted_count": 42
  }
}

The estimated_deleted_count field (for recursive deletes) contains the estimated number of items (files and directories) deleted (from vector index). The CLI will display this information in output.

When deleting viking://resources/..., the response may include memory_cleanup, indicating that user memories referencing that resource URI were cleaned up before deletion.

On case-insensitive localfs, case-only aliases may affect the delete target. For example, if the real file is viking://resources/bob-private/private.md, ov rm viking://resources/BOB-PRIVATE/private.md may delete the same file; on case-sensitive systems, deleting the missing alias path still succeeds because rm is idempotent, but it does not delete the real file.


cp() ​

Copy a file or directory to a new Viking URI. The source remains unchanged. Existing vector records under the source URI are copied and rewritten for the destination, so the copied content does not need to be parsed, described by a VLM, or embedded again.

The destination parent directory must already exist. Existing files are overwritten; existing directories are merged recursively, preserving destination-only files. to_uri is the exact destination, without appending the source directory name. File/directory type conflicts are rejected. Copying a directory requires recursive=true (or -r in the CLI). Source and destination must be distinct and neither may contain the other. Overwrite preserves the destination ACL; new entries inherit permissions from their destination parent.

Files use Exact Locks on both paths; directories use Tree Locks on both subtrees, without locking their parent trees. A content-copy failure can leave a partial destination. A vector-copy failure attempts to remove copied vectors and destination data. Existing destination contents are not backed up: rollback after a merge can delete the entire destination, including its preexisting contents. This is not an atomic transaction.

Parameters

ParameterTypeRequiredDefaultDescription
from_uristrYes-Source Viking URI
to_uristrYes-Destination Viking URI, including the new file or directory name
recursiveboolNoFalseRequired when the source is a directory

HTTP API

POST /api/v1/fs/cp
bash
# Copy one file
curl -X POST http://localhost:1933/api/v1/fs/cp \
  -H "Content-Type: application/json" \
  -H "X-API-Key: your-key" \
  -d '{
    "from_uri": "viking://resources/docs/guide.md",
    "to_uri": "viking://resources/archive/guide-copy.md",
    "recursive": false
  }'

# Copy a directory recursively
curl -X POST http://localhost:1933/api/v1/fs/cp \
  -H "Content-Type: application/json" \
  -H "X-API-Key: your-key" \
  -d '{
    "from_uri": "viking://resources/docs",
    "to_uri": "viking://resources/docs-backup",
    "recursive": true
  }'

CLI

bash
# Copy one file
ov cp viking://resources/docs/guide.md viking://resources/archive/guide-copy.md

# Copy a directory recursively
ov cp -r viking://resources/docs viking://resources/docs-backup

Response

json
{
  "status": "ok",
  "result": {
    "operation_id": "61ec2a80bf5f46a28aa3497fbdcb56dd",
    "operation": "copy",
    "from": "viking://resources/docs/guide.md",
    "to": "viking://resources/archive/guide-copy.md",
    "recursive": false,
    "phase": "completed",
    "files_created": 1,
    "vectors": {
      "scanned": 3,
      "written": 3,
      "deleted": 0,
      "restored": 0,
      "batches": 1
    },
    "semantic_root_uri": "viking://resources/archive",
    "semantic_status": "queued"
  }
}

semantic_status: "queued" means the copy has already committed and the destination parent's overview and abstract will be rebuilt asynchronously from summaries available at the destination. The API does not wait for that refresh. A refresh enqueue failure may return semantic_status: "failed" and semantic_error; it does not roll back the completed file and vector copy.

Common errors include NOT_FOUND when the source or destination parent is missing, CONFLICT when a path lock is busy, and INVALID_ARGUMENT (HTTP 400) when a directory is copied or removed without recursive=true, a directory operation targets a file, or the source/destination relationship or file/directory types are invalid.

On case-insensitive localfs, both from_uri and to_uri may be affected by case-only aliases. For example, ov cp viking://resources/BOB-PRIVATE/private.md viking://resources/archive/copy.md may copy from the real bob-private/private.md; a to_uri that differs only by case may also overwrite an existing target. On case-sensitive systems, a differently cased missing source returns NOT_FOUND, and a differently cased target is a separate path.


mv() ​

Move a file or directory. Existing files are overwritten; existing directories are merged recursively, preserving destination-only entries. to_uri is the exact destination without appending the source directory name. Type conflicts and overlapping source/destination paths are rejected.

Files use two Exact Locks; directories use Tree Locks on the source and destination, not their parent trees. The operation copies destination data, moves vector records, then deletes source data. Content-copy failures leave partial destinations. Vector or ACL update failures attempt to restore source vectors and remove destination data. A final source-deletion failure leaves the destination and any remaining source data; it does not rebuild the source. Old destination contents are not backed up, and rollback may remove an entire merged destination, so failure does not guarantee restoration of the original state.

Parameters

ParameterTypeRequiredDefaultDescription
from_uristrYes-Source Viking URI
to_uristrYes-Destination Viking URI

Python SDK (HTTP)

python
client.mv(
    from_uri="viking://resources/old-name/",
    to_uri="viking://resources/new-name/",
)

TypeScript SDK

typescript
await client.move(
  "viking://resources/docs/old.md",
  "viking://resources/docs/new.md",
);

Go SDK

go
if err := client.Move(ctx, "viking://resources/old-name/", "viking://resources/new-name/"); err != nil {
    return err
}

HTTP API

POST /api/v1/fs/mv
bash
curl -X POST http://localhost:1933/api/v1/fs/mv \
  -H "Content-Type: application/json" \
  -H "X-API-Key: your-key" \
  -d '{
    "from_uri": "viking://resources/old-name/",
    "to_uri": "viking://resources/new-name/"
  }'

CLI

bash
ov mv viking://resources/old-name/ viking://resources/new-name/

Response

json
{
  "status": "ok",
  "result": {
    "from": "viking://resources/old-name/",
    "to": "viking://resources/new-name/"
  }
}

On case-insensitive localfs, both from_uri and to_uri may be affected by case-only aliases. For example, ov mv viking://resources/BOB-PRIVATE/private.md viking://resources/archive/private.md may move the real bob-private/private.md; a to_uri that differs only by case may also overwrite or merge into an existing target. On case-sensitive systems, a differently cased missing source returns NOT_FOUND, and a differently cased target is a separate path.

Open source under the AGPL-3.0 License. Font licenses