Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions .agents/skills/use-ocis-cli/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
name: use-ocis-cli
description: Safely operate oCIS servers through the installed ocis command-line client. Use when an AI agent is asked to inspect, list, search, transfer, synchronize, share, restore, manage notifications, or administer files, Spaces, shares, users, or groups in oCIS. Do not use for developing the ocis-cli source code.
description: Safely operate oCIS servers through the installed ocis command-line client. Use when an AI agent is asked to inspect, list, search, transfer, synchronize, share, restore, inspect activity history, manage notifications, or administer files, Spaces, shares, users, or groups in oCIS. Do not use for developing the ocis-cli source code.
---

# Use oCIS CLI
Expand Down Expand Up @@ -44,8 +44,8 @@ protocol-level work.
parse human-readable tables when structured output is available.
- Use read-only discovery commands such as `ls`, `stat`, `search`, `tree`,
`space list`, `share overview`, `federation connection list`, `trash list`,
`notification list`, and admin `list` or `info` commands to resolve names and
IDs before changing anything.
`activity list`, `notification list`, and admin `list` or `info` commands to
resolve names and IDs before changing anything.
- Interpret a remote path in the selected Space. Keep local filesystem paths and
remote oCIS paths distinct according to the command help.

Expand All @@ -61,6 +61,9 @@ protocol-level work.
file or folder is a separate explicit operation.
- Use `trash` for recoverable deletion management and `version` for historical
file versions.
- Use `activity list` for read-only account-wide or resource-scoped history.
Pass a remote path or `--space` when the requested scope is narrower than the
account.
- Use `notification list` and `notification info` to inspect unread events.
In oCIS, `notification dismiss` is the server's mark-as-read operation; it
does not delete the resource referenced by the notification.
Expand Down
12 changes: 12 additions & 0 deletions .agents/skills/use-ocis-cli/references/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ same command.
- Spaces
- Sharing
- Federation
- Activity history
- Notifications
- Metadata, trash, and versions
- Administration
Expand Down Expand Up @@ -117,6 +118,17 @@ An invitation token establishes identity trust; it does not share a resource.
Treat invitation tokens as secrets and never accept one without an explicit
user request.

## Activity history

| Command | Purpose |
| --- | --- |
| `activity list, activity ls` | List account-wide or resource-scoped activity history with server-enforced permissions. |

With no path or explicit `--space`, `activity list` is account-wide. Pass a
remote path to use the current file root, or pass `--space SPACE` to scope the
query to that Space. Use `--depth`, `--limit`, and `--sort` for server-side
filtering. Activity history is read-only.

## Notifications

| Command | Purpose |
Expand Down
9 changes: 6 additions & 3 deletions .claude/skills/use-ocis-cli/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
name: use-ocis-cli
description: Safely operate oCIS servers through the installed ocis command-line client. Use when an AI agent is asked to inspect, list, search, transfer, synchronize, share, restore, manage notifications, or administer files, Spaces, shares, users, or groups in oCIS. Do not use for developing the ocis-cli source code.
description: Safely operate oCIS servers through the installed ocis command-line client. Use when an AI agent is asked to inspect, list, search, transfer, synchronize, share, restore, inspect activity history, manage notifications, or administer files, Spaces, shares, users, or groups in oCIS. Do not use for developing the ocis-cli source code.
---

# Use oCIS CLI
Expand Down Expand Up @@ -44,8 +44,8 @@ protocol-level work.
parse human-readable tables when structured output is available.
- Use read-only discovery commands such as `ls`, `stat`, `search`, `tree`,
`space list`, `share overview`, `federation connection list`, `trash list`,
`notification list`, and admin `list` or `info` commands to resolve names and
IDs before changing anything.
`activity list`, `notification list`, and admin `list` or `info` commands to
resolve names and IDs before changing anything.
- Interpret a remote path in the selected Space. Keep local filesystem paths and
remote oCIS paths distinct according to the command help.

Expand All @@ -61,6 +61,9 @@ protocol-level work.
file or folder is a separate explicit operation.
- Use `trash` for recoverable deletion management and `version` for historical
file versions.
- Use `activity list` for read-only account-wide or resource-scoped history.
Pass a remote path or `--space` when the requested scope is narrower than the
account.
- Use `notification list` and `notification info` to inspect unread events.
In oCIS, `notification dismiss` is the server's mark-as-read operation; it
does not delete the resource referenced by the notification.
Expand Down
12 changes: 12 additions & 0 deletions .claude/skills/use-ocis-cli/references/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ same command.
- Spaces
- Sharing
- Federation
- Activity history
- Notifications
- Metadata, trash, and versions
- Administration
Expand Down Expand Up @@ -117,6 +118,17 @@ An invitation token establishes identity trust; it does not share a resource.
Treat invitation tokens as secrets and never accept one without an explicit
user request.

## Activity history

| Command | Purpose |
| --- | --- |
| `activity list, activity ls` | List account-wide or resource-scoped activity history with server-enforced permissions. |

With no path or explicit `--space`, `activity list` is account-wide. Pass a
remote path to use the current file root, or pass `--space SPACE` to scope the
query to that Space. Use `--depth`, `--limit`, and `--sort` for server-side
filtering. Activity history is read-only.

## Notifications

| Command | Purpose |
Expand Down
8 changes: 6 additions & 2 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ The repository follows the standard Go command layout:
cmd/
ocis/ executable entrypoint only
internal/
activities/ authenticated oCIS Graph activity-history client
command/ Cobra command tree and input validation
app/ application use-case orchestration
apperror/ stable error categories and exit-code mapping
Expand Down Expand Up @@ -58,7 +59,7 @@ without starting a subprocess.
`batch_service.go`, `filesystem_service.go`, `filesystem_tree_service.go`,
`filesystem_du_service.go`, `filesystem_touch_service.go`,
`filesystem_walk.go`, `metadata_service.go`,
`notification_service.go`,
`activity_service.go`, `notification_service.go`,
`share_overview_service.go`,
`space_member_service.go`, `space_update_service.go`,
`space_lifecycle_service.go`, and
Expand All @@ -67,6 +68,9 @@ without starting a subprocess.
contains shared application wiring.
- `internal/apperror`: classify usage, authentication, not-found, and conflict
errors without coupling application services to Cobra.
- `internal/activities`: query account-wide or resource-scoped activity
history through the bounded oCIS Graph extension, preserving the localized
message template and structured variables returned by the server.
- `internal/auth`: implement OIDC discovery, dynamic native-client
registration, token exchange, refresh, and userinfo.
- `internal/config`: validate server URLs, requiring `https` unless the caller
Expand Down Expand Up @@ -135,7 +139,7 @@ without starting a subprocess.
authentication headers, metadata and checksum response mapping, and safe
scalar custom-property `PROPFIND`/`PROPPATCH` operations.

Protocol-specific behavior belongs in dedicated `internal/auth`,
Protocol-specific behavior belongs in dedicated `internal/activities`, `internal/auth`,
`internal/federation`, `internal/graph`, `internal/notifications`,
`internal/search`, `internal/sharing`, `internal/trash`, `internal/versions`,
and `internal/webdav` adapters. Recursive local/remote
Expand Down
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ check: fmt

coverage:
go run ./tools/covercheck -min $(COVERAGE_MIN) \
app auth federation graph httpapi notifications retry search sharing sync trash transfer versions \
activities app auth federation graph httpapi notifications retry search sharing sync trash transfer versions \
webdav

fmt:
Expand Down
38 changes: 38 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1204,6 +1204,44 @@ sharing restrictions, and resource permissions. A user may be able to read a
file without being allowed to share it, update a share, or remove another
user's permission.

## Activity history

Inspect changes recorded by the oCIS activity service. With no path or explicit
`--space`, the command returns account-wide activity visible to the current
user:

```sh
ocis activity list
ocis activity list --limit 50 --sort desc
ocis --json activity list
```

Pass a remote path to scope the history to a file or folder. Path resolution
uses the profile's saved default Space, or the personal file root when no Space
is selected. An explicit `--space` without a path scopes the query to that
Space's root:

```sh
ocis activity list /reports/report.pdf
ocis activity list /projects --depth 1
ocis --space Engineering activity list
ocis --space Engineering activity list /reports --depth -1
```

`--depth 0` selects only the resolved resource, positive values include that
many descendant levels, and `--depth -1` includes the complete recorded
subtree. The default limit is 100; `--limit -1` requests all history retained
by the server. Sorting accepts `asc` or `desc` and defaults to newest first.

Human output substitutes the structured activity variables into the server's
localized message. JSON and JSONL preserve the activity ID, recorded time,
message template, and complete variables for automation.

Activity history is read-only. The server requires the authenticated user to
have permission to list grants on the selected resource, so access can differ
between users and Spaces. The CLI reports that authorization decision instead
of assuming that every authenticated user can inspect every activity.

## Notifications

List and inspect the authenticated user's unread in-app notifications:
Expand Down
133 changes: 133 additions & 0 deletions internal/activities/client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
// Package activities implements the authenticated oCIS Graph activity API.
package activities

import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"strconv"
"strings"

"github.com/mzner/ocis-cli/internal/httpapi"
)

const (
endpoint = "/graph/v1beta1/extensions/org.libregraph/activities"
maxResponseBytes = 8 << 20
maxLimit = 1000
)

// Activity is one server-recorded resource activity.
type Activity struct {
ID string `json:"id"`
Times Times `json:"times"`
Template Template `json:"template"`
}

// Times contains server timestamps associated with an activity.
type Times struct {
RecordedTime string `json:"recordedTime"`
}

// Template contains the localized activity message and its structured values.
type Template struct {
Message string `json:"message"`
Variables map[string]any `json:"variables,omitempty"`
}

// ListRequest describes server-side activity filters. A nil Depth omits the
// filter; -1 explicitly requests the complete subtree.
type ListRequest struct {
ItemID string
Depth *int
Limit int
Sort string
}

// Client reads activities visible to the authenticated user.
type Client struct {
api *httpapi.Client
}

// NewClient constructs an activities client.
func NewClient(config httpapi.Config, httpClient *http.Client) *Client {
return &Client{api: httpapi.NewClient(config, httpClient)}
}

// List returns activities matching the requested resource and bounds.
func (client *Client) List(
ctx context.Context, request ListRequest,
) ([]Activity, error) {
resource, err := listResource(request)
if err != nil {
return nil, err
}
response, err := client.api.Do(
ctx, http.MethodGet, resource, nil,
http.Header{"Accept": {"application/json"}},
)
if err != nil {
return nil, err
}
defer func() { _ = response.Body.Close() }()
if response.StatusCode < 200 || response.StatusCode >= 300 {
return nil, httpapi.ResponseError(response)
}
data, err := io.ReadAll(io.LimitReader(response.Body, maxResponseBytes))
if err != nil {
return nil, fmt.Errorf("read activities response: %w", err)
}
var payload struct {
Value []Activity `json:"value"`
}
if err := json.Unmarshal(data, &payload); err != nil {
return nil, fmt.Errorf("decode activities response: %w", err)
}
if payload.Value == nil {
return []Activity{}, nil
}
return payload.Value, nil
}

func listResource(request ListRequest) (string, error) {
filters := make([]string, 0, 4)
itemID := strings.TrimSpace(request.ItemID)
if strings.ContainsAny(itemID, "\"\r\n") {
return "", errors.New("activity item ID contains unsupported characters")
}
if itemID != "" {
filters = append(filters, `itemid:"`+itemID+`"`)
}
if request.Depth != nil {
if *request.Depth < -1 {
return "", errors.New("activity depth must be -1 or greater")
}
filters = append(filters, "depth:"+strconv.Itoa(*request.Depth))
}
if request.Limit != 0 {
if request.Limit != -1 &&
(request.Limit < 1 || request.Limit > maxLimit) {
return "", fmt.Errorf(
"activity limit must be -1 or between 1 and %d", maxLimit,
)
}
filters = append(filters, "limit:"+strconv.Itoa(request.Limit))
}
sortOrder := strings.ToLower(strings.TrimSpace(request.Sort))
if sortOrder != "" {
if sortOrder != "asc" && sortOrder != "desc" {
return "", errors.New("activity sort must be asc or desc")
}
filters = append(filters, "sort:"+sortOrder)
}
if len(filters) == 0 {
return endpoint, nil
}
query := url.Values{}
query.Set("kql", strings.Join(filters, " AND "))
return endpoint + "?" + query.Encode(), nil
}
Loading