activecollab

package module
v0.3.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 4, 2026 License: MIT Imports: 11 Imported by: 0

README

activecollab-cli

Unofficial command-line client for ActiveCollab tasks and coding workflows. It is designed for both humans and automation, with noninteractive commands and a stable JSON output mode.

The first release targets ActiveCollab Self-Hosted 7.4.765 and its /api/v1 API. Other server versions are not yet tested or claimed as compatible.

Agent skill

An AI-agent workflow skill for this CLI lives in the microHoffman/agent-skills repository. The skill reads task context before coding and requires explicit authorization before it posts comments, updates fields, or completes tasks.

Install it independently of the CLI:

npx skills add https://github.com/microHoffman/agent-skills \
  --skill activecollab \
  --agent '*' \
  --global \
  --yes

Capabilities

  • list projects, users, task lists, and project tasks
  • read task details, comments, subtasks, history, and task/comment attachments
  • create and update tasks and subtasks
  • add and update comments
  • complete and reopen tasks and subtasks
  • upload and safely download task/comment attachments
  • preview mutations with --dry-run
  • emit machine-readable output with --json
  • issue and store self-hosted API tokens in a protected per-user file

Deletion, arbitrary raw API requests, invoicing, and time tracking are outside the initial scope.

Installation

With mise:

mise use --global github:microHoffman/activecollab-cli@0.3.1
activecollab version

Without mise, download and verify the archive for your OS and architecture from GitHub Releases. Exact Linux, macOS, Windows, and source installation instructions are in the installation guide.

Go users can install from source:

go install github.com/microHoffman/activecollab-cli/cmd/activecollab@v0.3.1

Documentation

Configuration

For a self-hosted installation, pass the complete API-v1 URL and log in:

activecollab auth login \
  --url https://activecollab.example.com/api/v1
activecollab info

The login command prompts for the account email and a hidden password, requests an API token from the self-hosted server, and saves the server URL, account, and token in a protected per-user credentials file. The credentials directory and file use owner-only permissions on Linux and macOS; Windows uses a protected DACL for the current user, SYSTEM, and Administrators. HTTPS is required unless --allow-insecure-http is explicitly passed. This follows ActiveCollab's self-hosted authentication flow.

If ActiveCollab already issued a token, pipe it from a secret manager instead of putting it in shell history:

secret-manager-command | activecollab auth login \
  --url https://activecollab.example.com/api/v1 \
  --token-stdin

Use activecollab auth status to inspect the active source without exposing the token. activecollab auth logout removes the local credential but does not revoke it on the server.

For CI or ephemeral sessions, environment variables remain supported and override saved credentials:

export ACTIVECOLLAB_URL="https://activecollab.example.com/api/v1"
export ACTIVECOLLAB_TOKEN="..."

Never pass a password or token as a command-line argument, commit it, or paste it into an agent conversation.

Examples

# Discover projects and tasks
activecollab project list
activecollab task list --project 7

# A pasted task URL supplies both IDs
activecollab task get \
  https://activecollab.example.com/projects/7/tasks/22

# Frontend modal links are accepted too (Task-{task_id}-{project_id})
activecollab task get \
  'https://activecollab.example.com/my-work?modal=Task-22-7'

# A numeric task ID requires its project
activecollab task get 22 --project 7 --json

# Create a task with a multiline body and attachment
activecollab task create \
  --project 7 \
  --name "Add contract tests" \
  --body-file task-description.md \
  --assignee-id 9 \
  --attach specification.txt

# Preview a write without making any HTTP request
activecollab task update 22 \
  --project 7 \
  --name "Updated name" \
  --dry-run \
  --json

# Add a comment from stdin
printf '%s\n' 'Implemented and verified with go test ./...' |
  activecollab comment add 22 --project 7 --body-file -

# Complete only when explicitly intended
activecollab task complete 22 --project 7

Run activecollab <resource> <command> --help for all flags.

Output

Human-readable formatted JSON is the default. --json provides stable envelopes for automation:

{"data":{"id":22,"project_id":7,"name":"Example"}}

Failures are written to stderr and use this shape in JSON mode:

{"error":{"code":"api_error","message":"...","http_status":404}}

Compatibility design

ActiveCollab's official API documentation describes the /api/v1 contract used by the target self-hosted release. The client therefore implements one API-v1 contract instead of guessing at product-version adapters. Wire responses are mapped to normalized CLI types and tolerate additive JSON fields.

When another server version is evaluated, the same contract suite will run against sanitized fixtures from that version. Version-specific endpoint mapping will be added only when an actual incompatibility is demonstrated.

That is also the path for future v8 support: add an exact-version fixture set and register the version only after the shared API-v1 contract passes. Introduce a v8-specific mapper only if verified responses or endpoints actually differ.

Development and tests

The repository pins Go 1.26.5 for mise users and declares Go 1.25 as the minimum:

mise install
mise run check

Or with an existing supported Go installation:

go vet ./...
go test -race ./...

Automated tests use only in-process fake HTTP servers and synthetic fixtures. They never connect to or modify a real ActiveCollab workspace. Before a release, the only manual server check is read-only: info, project list, and task get for an explicitly selected task.

Command reference pages are generated from the runtime Cobra command tree. When command metadata or flags change, regenerate and verify them with:

mise run docs
mise run docs-check

License

MIT. ActiveCollab is a trademark of its respective owner; this project is not officially affiliated with or endorsed by ActiveCollab.

Documentation

Index

Constants

View Source
const TestedServerVersion = "7.4.765"

Variables

This section is empty.

Functions

func IsTestedServerVersion

func IsTestedServerVersion(version string) bool

Types

type APIError

type APIError struct {
	StatusCode int
	Type       string
	Message    string
}

func (*APIError) Error

func (e *APIError) Error() string

type Attachment

type Attachment struct {
	ID           int    `json:"id"`
	Name         string `json:"name"`
	MIMEType     string `json:"mime_type,omitempty"`
	Size         int64  `json:"size,omitempty"`
	Disposition  string `json:"disposition,omitempty"`
	DownloadURL  string `json:"download_url,omitempty"`
	ThumbnailURL string `json:"thumbnail_url,omitempty"`
}

type Client

type Client struct {
	// contains filtered or unexported fields
}

func NewClient

func NewClient(config Config) (*Client, error)

func (*Client) AddComment

func (c *Client) AddComment(ctx context.Context, taskID int, input CommentCreateInput) (Comment, error)

func (*Client) CompleteSubtask

func (c *Client) CompleteSubtask(ctx context.Context, ref TaskRef, subtaskID int) (Subtask, error)

func (*Client) CompleteTask

func (c *Client) CompleteTask(ctx context.Context, taskID int) (Task, error)

func (*Client) CreateSubtask

func (c *Client) CreateSubtask(ctx context.Context, ref TaskRef, input SubtaskCreateInput) (Subtask, error)

func (*Client) CreateTask

func (c *Client) CreateTask(ctx context.Context, projectID int, input TaskCreateInput) (Task, error)

func (*Client) DownloadAttachment

func (c *Client) DownloadAttachment(ctx context.Context, attachment Attachment, target io.Writer) (DownloadResult, error)

func (*Client) GetProject

func (c *Client) GetProject(ctx context.Context, projectID int) (Project, error)

func (*Client) GetSubtask

func (c *Client) GetSubtask(ctx context.Context, ref TaskRef, subtaskID int) (Subtask, error)

func (*Client) GetTask

func (c *Client) GetTask(ctx context.Context, ref TaskRef) (Task, error)

func (*Client) GetUser

func (c *Client) GetUser(ctx context.Context, userID int) (User, error)

func (*Client) Info

func (c *Client) Info(ctx context.Context) (Info, error)

func (*Client) ListAttachments

func (c *Client) ListAttachments(ctx context.Context, ref TaskRef) ([]Attachment, error)

func (*Client) ListComments

func (c *Client) ListComments(ctx context.Context, ref TaskRef) ([]Comment, error)

func (*Client) ListProjects

func (c *Client) ListProjects(ctx context.Context) ([]Project, error)

func (*Client) ListSubtasks

func (c *Client) ListSubtasks(ctx context.Context, ref TaskRef) ([]Subtask, error)

func (*Client) ListTaskLists

func (c *Client) ListTaskLists(ctx context.Context, projectID int) ([]TaskList, error)

func (*Client) ListTasks

func (c *Client) ListTasks(ctx context.Context, projectID int) ([]Task, error)

func (*Client) ListUsers

func (c *Client) ListUsers(ctx context.Context) ([]User, error)

func (*Client) ReopenSubtask

func (c *Client) ReopenSubtask(ctx context.Context, ref TaskRef, subtaskID int) (Subtask, error)

func (*Client) ReopenTask

func (c *Client) ReopenTask(ctx context.Context, taskID int) (Task, error)

func (*Client) ResolveTaskRef

func (c *Client) ResolveTaskRef(value string, projectID int) (TaskRef, error)

func (*Client) TaskHistory

func (c *Client) TaskHistory(ctx context.Context, taskID int, verbose bool) ([]HistoryEntry, error)

func (*Client) UpdateComment

func (c *Client) UpdateComment(ctx context.Context, commentID int, input CommentUpdateInput) (Comment, error)

func (*Client) UpdateSubtask

func (c *Client) UpdateSubtask(ctx context.Context, ref TaskRef, subtaskID int, input SubtaskUpdateInput) (Subtask, error)

func (*Client) UpdateTask

func (c *Client) UpdateTask(ctx context.Context, ref TaskRef, input TaskUpdateInput) (Task, error)

type Comment

type Comment struct {
	ID            int          `json:"id"`
	ParentType    string       `json:"parent_type,omitempty"`
	ParentID      int          `json:"parent_id,omitempty"`
	Body          string       `json:"body"`
	BodyPlainText string       `json:"body_plain_text,omitempty"`
	CreatedOn     int64        `json:"created_on,omitempty"`
	CreatedByID   int          `json:"created_by_id,omitempty"`
	UpdatedOn     int64        `json:"updated_on,omitempty"`
	Attachments   []Attachment `json:"attachments,omitempty"`
}

type CommentCreateInput

type CommentCreateInput struct {
	Body        string   `json:"body"`
	Attachments []string `json:"attachments,omitempty"`
}

type CommentUpdateInput

type CommentUpdateInput struct {
	Body string `json:"body"`
}

type Config

type Config struct {
	BaseURL    string
	Token      string
	HTTPClient *http.Client
	UserAgent  string
}

type DownloadResult

type DownloadResult struct {
	Size        int64  `json:"size"`
	ContentType string `json:"content_type,omitempty"`
}

type HistoryEntry

type HistoryEntry struct {
	Timestamp      int64            `json:"timestamp"`
	CreatedByID    int              `json:"created_by_id,omitempty"`
	CreatedByName  string           `json:"created_by_name,omitempty"`
	CreatedByEmail string           `json:"created_by_email,omitempty"`
	Modifications  map[string][]any `json:"modifications"`
}

type Info

type Info struct {
	Application string `json:"application"`
	Version     string `json:"version"`
	Tested      bool   `json:"tested"`
}

type Project

type Project struct {
	ID          int    `json:"id"`
	Name        string `json:"name"`
	Body        any    `json:"body,omitempty"`
	IsCompleted bool   `json:"is_completed"`
	IsTrashed   bool   `json:"is_trashed"`
	LeaderID    int    `json:"leader_id,omitempty"`
}

type Subtask

type Subtask struct {
	ID          int    `json:"id"`
	TaskID      int    `json:"task_id"`
	ProjectID   int    `json:"project_id"`
	Name        string `json:"name"`
	AssigneeID  int    `json:"assignee_id,omitempty"`
	IsCompleted bool   `json:"is_completed"`
	DueOn       any    `json:"due_on,omitempty"`
	CreatedOn   int64  `json:"created_on,omitempty"`
	UpdatedOn   int64  `json:"updated_on,omitempty"`
}

type SubtaskCreateInput

type SubtaskCreateInput struct {
	Name       string  `json:"name"`
	AssigneeID *int    `json:"assignee_id,omitempty"`
	DueOn      *string `json:"due_on,omitempty"`
}

type SubtaskUpdateInput

type SubtaskUpdateInput struct {
	Name          *string `json:"name,omitempty"`
	AssigneeID    *int    `json:"assignee_id,omitempty"`
	ClearAssignee bool    `json:"clear_assignee,omitempty"`
	DueOn         *string `json:"due_on,omitempty"`
	ClearDueOn    bool    `json:"clear_due_on,omitempty"`
}

type Task

type Task struct {
	ID                int          `json:"id"`
	ProjectID         int          `json:"project_id"`
	TaskNumber        int          `json:"task_number,omitempty"`
	TaskListID        int          `json:"task_list_id,omitempty"`
	Name              string       `json:"name"`
	Body              string       `json:"body,omitempty"`
	BodyPlainText     string       `json:"body_plain_text,omitempty"`
	AssigneeID        int          `json:"assignee_id,omitempty"`
	IsCompleted       bool         `json:"is_completed"`
	IsImportant       bool         `json:"is_important"`
	IsTrashed         bool         `json:"is_trashed"`
	DueOn             any          `json:"due_on,omitempty"`
	CreatedOn         int64        `json:"created_on,omitempty"`
	UpdatedOn         int64        `json:"updated_on,omitempty"`
	CommentsCount     int          `json:"comments_count,omitempty"`
	TotalSubtasks     int          `json:"total_subtasks,omitempty"`
	CompletedSubtasks int          `json:"completed_subtasks,omitempty"`
	OpenSubtasks      int          `json:"open_subtasks,omitempty"`
	Attachments       []Attachment `json:"attachments,omitempty"`
	Comments          []Comment    `json:"comments,omitempty"`
	Subtasks          []Subtask    `json:"subtasks,omitempty"`
	TaskList          *TaskList    `json:"task_list,omitempty"`
}

type TaskCreateInput

type TaskCreateInput struct {
	Name        string   `json:"name"`
	Body        *string  `json:"body,omitempty"`
	AssigneeID  *int     `json:"assignee_id,omitempty"`
	DueOn       *string  `json:"due_on,omitempty"`
	TaskListID  *int     `json:"task_list_id,omitempty"`
	IsImportant *bool    `json:"is_important,omitempty"`
	Attachments []string `json:"attachments,omitempty"`
}

type TaskList

type TaskList struct {
	ID          int    `json:"id"`
	ProjectID   int    `json:"project_id"`
	Name        string `json:"name"`
	IsCompleted bool   `json:"is_completed"`
	OpenTasks   int    `json:"open_tasks,omitempty"`
}

type TaskRef

type TaskRef struct {
	ProjectID int `json:"project_id"`
	TaskID    int `json:"task_id"`
}

func ParseTaskRef

func ParseTaskRef(value string, projectID int) (TaskRef, error)

type TaskUpdateInput

type TaskUpdateInput struct {
	Name          *string  `json:"name,omitempty"`
	Body          *string  `json:"body,omitempty"`
	AssigneeID    *int     `json:"assignee_id,omitempty"`
	ClearAssignee bool     `json:"clear_assignee,omitempty"`
	DueOn         *string  `json:"due_on,omitempty"`
	ClearDueOn    bool     `json:"clear_due_on,omitempty"`
	TaskListID    *int     `json:"task_list_id,omitempty"`
	ClearTaskList bool     `json:"clear_task_list,omitempty"`
	IsImportant   *bool    `json:"is_important,omitempty"`
	Attachments   []string `json:"attachments,omitempty"`
}

type Upload

type Upload struct {
	Code         string `json:"code"`
	Name         string `json:"name"`
	MIMEType     string `json:"mime_type,omitempty"`
	Size         int64  `json:"size,omitempty"`
	ThumbnailURL string `json:"thumbnail_url,omitempty"`
}

type User

type User struct {
	ID               int    `json:"id"`
	DisplayName      string `json:"display_name"`
	ShortDisplayName string `json:"short_display_name,omitempty"`
	Email            string `json:"email,omitempty"`
	IsArchived       bool   `json:"is_archived"`
	IsTrashed        bool   `json:"is_trashed"`
}

Directories

Path Synopsis
cmd
activecollab command
gen-docs command
Command gen-docs generates the committed CLI command reference from the same Cobra command tree used by the activecollab binary.
Command gen-docs generates the committed CLI command reference from the same Cobra command tree used by the activecollab binary.
internal
cli

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL