commit e57ef2c25d4fde17925e75253561c0353355d596 Author: Twentyninehairs_bot Date: Sun May 3 18:11:31 2026 -0700 Initial commit: NextCloud file operations skill diff --git a/nextcloud.mjs b/nextcloud.mjs new file mode 100644 index 0000000..c05a23b --- /dev/null +++ b/nextcloud.mjs @@ -0,0 +1,167 @@ +import { tool } from "@opencode-ai/plugin/tool"; +import https from "node:https"; +import fs from "node:fs"; +import path from "node:path"; + +const NC_URL = "https://nc.hibbhome.com"; +const NC_USER = "opencode_memgpt"; +const NC_PASS = "ioH2o-QnQJx-8z7Dx-edPyx-pmxLA"; +const DAV_BASE = `${NC_URL}/remote.php/dav/files/${NC_USER}`; + +function authHeader() { + return "Basic " + Buffer.from(`${NC_USER}:${NC_PASS}`).toString("base64"); +} + +function davRequest(method, remotePath, body = null) { + return new Promise((resolve, reject) => { + const url = new URL(`${DAV_BASE}${remotePath}`); + const opts = { + hostname: url.hostname, + port: 443, + path: url.pathname, + method, + headers: { + Authorization: authHeader(), + }, + }; + if (body) { + opts.headers["Content-Length"] = Buffer.byteLength(body); + } + const req = https.request(opts, (res) => { + const chunks = []; + res.on("data", (c) => chunks.push(c)); + res.on("end", () => { + resolve({ + status: res.statusCode, + headers: res.headers, + body: Buffer.concat(chunks), + }); + }); + }); + req.on("error", reject); + if (body) req.write(body); + req.end(); + }); +} + +export const NextcloudPlugin = async (_ctx) => { + return { + tool: { + nextcloud_upload: tool({ + description: "Upload a local file to NextCloud", + args: { + local_path: tool.schema.string().describe("Local file path to upload"), + remote_path: tool.schema.string().describe("Remote path on NextCloud (e.g., /Documents/file.txt)"), + }, + async execute(args) { + const content = fs.readFileSync(args.local_path); + const res = davRequest("PUT", args.remote_path, content); + return (await res).status === 201 || (await res).status === 204 + ? `Uploaded ${args.local_path} to ${args.remote_path}` + : `Upload failed with status ${(await res).status}`; + }, + }), + + nextcloud_download: tool({ + description: "Download a file from NextCloud to local disk", + args: { + remote_path: tool.schema.string().describe("Remote path on NextCloud"), + local_path: tool.schema.string().describe("Local file path to save to"), + }, + async execute(args) { + const res = await davRequest("GET", args.remote_path); + if (res.status === 200) { + const dir = path.dirname(args.local_path); + if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true }); + fs.writeFileSync(args.local_path, res.body); + return `Downloaded ${args.remote_path} to ${args.local_path}`; + } + return `Download failed with status ${res.status}`; + }, + }), + + nextcloud_list: tool({ + description: "List files in a NextCloud directory", + args: { + remote_path: tool.schema.string().describe("Remote directory path on NextCloud (e.g., /Documents)"), + }, + async execute(args) { + const body = ` + + +`; + const res = await davRequest("PROPFIND", args.remote_path, body); + if (res.status === 207) { + const xml = res.body.toString(); + const items = []; + const regex = /(.*?)<\/d:displayname>/g; + let match; + while ((match = regex.exec(xml)) !== null) { + if (match[1]) items.push(match[1]); + } + return items.length > 0 ? items.join("\n") : "Empty directory"; + } + return `List failed with status ${res.status}`; + }, + }), + + nextcloud_mkdir: tool({ + description: "Create a directory on NextCloud", + args: { + remote_path: tool.schema.string().describe("Remote directory path to create"), + }, + async execute(args) { + const res = await davRequest("MKCOL", args.remote_path); + return res.status === 201 + ? `Created directory ${args.remote_path}` + : `mkdir failed with status ${res.status}`; + }, + }), + + nextcloud_delete: tool({ + description: "Delete a file or directory on NextCloud", + args: { + remote_path: tool.schema.string().describe("Remote path to delete"), + }, + async execute(args) { + const res = await davRequest("DELETE", args.remote_path); + return res.status === 204 + ? `Deleted ${args.remote_path}` + : `Delete failed with status ${res.status}`; + }, + }), + + nextcloud_copy: tool({ + description: "Copy a file within NextCloud", + args: { + source_path: tool.schema.string().describe("Source path on NextCloud"), + dest_path: tool.schema.string().describe("Destination path on NextCloud"), + }, + async execute(args) { + const url = new URL(`${DAV_BASE}${args.source_path}`); + const opts = { + hostname: url.hostname, + port: 443, + path: url.pathname, + method: "COPY", + headers: { + Authorization: authHeader(), + Destination: `${DAV_BASE}${args.dest_path}`, + }, + }; + const res = await new Promise((resolve, reject) => { + const req = https.request(opts, (r) => { + r.on("data", () => {}); + r.on("end", () => resolve(r.statusCode)); + }); + req.on("error", reject); + req.end(); + }); + return res === 201 || res === 204 + ? `Copied ${args.source_path} to ${args.dest_path}` + : `Copy failed with status ${res}`; + }, + }), + }, + }; +}; diff --git a/nextcloud.sh b/nextcloud.sh new file mode 100755 index 0000000..2566876 --- /dev/null +++ b/nextcloud.sh @@ -0,0 +1,31 @@ +#!/bin/bash +# NextCloud file operations helper +NC_URL="https://nc.hibbhome.com" +NC_USER="opencode_memgpt" +NC_PASS="ioH2o-QnQJx-8z7Dx-edPyx-pmxLA" +DAV_BASE="$NC_URL/remote.php/dav/files/$NC_USER" + +case "$1" in + upload) + curl -s -u "$NC_USER:$NC_PASS" "$DAV_BASE$3" -T "$2" + echo "Uploaded $2 to $3" + ;; + download) + curl -s -u "$NC_USER:$NC_PASS" "$DAV_BASE$2" -o "$3" + echo "Downloaded $2 to $3" + ;; + list) + curl -s -u "$NC_USER:$NC_PASS" "$DAV_BASE$2" -X PROPFIND -H "Depth: 1" | grep -o '[^<]*' | sed 's/<[^>]*>//g' + ;; + mkdir) + curl -s -u "$NC_USER:$NC_PASS" "$DAV_BASE$2" -X MKCOL + echo "Created directory $2" + ;; + delete) + curl -s -u "$NC_USER:$NC_PASS" "$DAV_BASE$2" -X DELETE + echo "Deleted $2" + ;; + *) + echo "Usage: nextcloud.sh {upload|download|list|mkdir|delete} [args]" + ;; +esac