aiquila-mcp 0.3.22 → 0.3.24
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/client/passman.js +41 -0
- package/dist/tool-registry.js +28 -0
- package/dist/tools/apps/announcements.js +178 -0
- package/dist/tools/apps/passman.js +150 -0
- package/dist/tools/apps/recommendations.js +74 -0
- package/dist/tools/apps/registration.js +164 -0
- package/dist/tools/apps/social-sharing.js +142 -0
- package/dist/tools/apps/terms-of-service.js +254 -0
- package/package.json +1 -1
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
import { getNextcloudConfig } from '../tools/types.js';
|
|
3
|
+
import { logger } from '../logger.js';
|
|
4
|
+
import { ApiError } from './aiquila.js';
|
|
5
|
+
/**
|
|
6
|
+
* Make an authenticated request to the Nextcloud Passman REST API v2.
|
|
7
|
+
*
|
|
8
|
+
* Base path: /index.php/apps/passman/api/v2
|
|
9
|
+
*/
|
|
10
|
+
export async function fetchPassmanAPI(endpoint, options = {}) {
|
|
11
|
+
const config = getNextcloudConfig();
|
|
12
|
+
const auth = Buffer.from(`${config.user}:${config.password}`).toString('base64');
|
|
13
|
+
let url = `${config.url}/index.php/apps/passman/api/v2${endpoint}`;
|
|
14
|
+
if (options.queryParams) {
|
|
15
|
+
const params = new URLSearchParams(options.queryParams);
|
|
16
|
+
url += `?${params.toString()}`;
|
|
17
|
+
}
|
|
18
|
+
const headers = {
|
|
19
|
+
Authorization: `Basic ${auth}`,
|
|
20
|
+
'OCS-APIRequest': 'true',
|
|
21
|
+
Accept: 'application/json',
|
|
22
|
+
};
|
|
23
|
+
let body;
|
|
24
|
+
if (options.body !== undefined) {
|
|
25
|
+
body = JSON.stringify(options.body);
|
|
26
|
+
headers['Content-Type'] = 'application/json';
|
|
27
|
+
}
|
|
28
|
+
const method = options.method ?? 'GET';
|
|
29
|
+
const t0 = Date.now();
|
|
30
|
+
const response = await fetch(url, { method, headers, body });
|
|
31
|
+
logger.trace({ method, url, status: response.status, ms: Date.now() - t0 }, '[passman] HTTP');
|
|
32
|
+
if (!response.ok) {
|
|
33
|
+
const text = await response.text();
|
|
34
|
+
throw new ApiError(response.status, response.statusText, text);
|
|
35
|
+
}
|
|
36
|
+
if (response.status === 200 &&
|
|
37
|
+
response.headers.get('content-type')?.includes('application/json')) {
|
|
38
|
+
return (await response.json());
|
|
39
|
+
}
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
package/dist/tool-registry.js
CHANGED
|
@@ -34,12 +34,18 @@ import { userStatusTools } from './tools/apps/user-status.js';
|
|
|
34
34
|
import { absenceTools } from './tools/apps/absence.js';
|
|
35
35
|
import { notificationsTools } from './tools/apps/notifications.js';
|
|
36
36
|
import { activityTools } from './tools/apps/activity.js';
|
|
37
|
+
import { announcementTools } from './tools/apps/announcements.js';
|
|
38
|
+
import { registrationTools } from './tools/apps/registration.js';
|
|
39
|
+
import { termsOfServiceTools } from './tools/apps/terms-of-service.js';
|
|
37
40
|
import { trashTools } from './tools/apps/trash.js';
|
|
38
41
|
import { versionsTools } from './tools/apps/versions.js';
|
|
39
42
|
import { projectsTools } from './tools/apps/projects.js';
|
|
40
43
|
import { pollsTools } from './tools/apps/polls.js';
|
|
41
44
|
import { formsTools } from './tools/apps/forms.js';
|
|
42
45
|
import { textTools } from './tools/apps/text.js';
|
|
46
|
+
import { recommendationsTools } from './tools/apps/recommendations.js';
|
|
47
|
+
import { socialSharingTools } from './tools/apps/social-sharing.js';
|
|
48
|
+
import { passmanTools } from './tools/apps/passman.js';
|
|
43
49
|
/**
|
|
44
50
|
* Single source of truth for tool-to-Nextcloud-app mapping.
|
|
45
51
|
*
|
|
@@ -74,6 +80,7 @@ export const TOOL_REGISTRY = [
|
|
|
74
80
|
{ category: 'news', appIds: ['news'], tools: newsTools },
|
|
75
81
|
{ category: 'mail', appIds: ['mail'], tools: mailTools },
|
|
76
82
|
{ category: 'deck', appIds: ['deck'], tools: deckTools },
|
|
83
|
+
{ category: 'passman', appIds: ['passman'], tools: passmanTools },
|
|
77
84
|
{ category: 'cookbook', appIds: ['cookbook'], tools: cookbookTools },
|
|
78
85
|
{ category: 'maps', appIds: ['maps'], tools: mapsTools },
|
|
79
86
|
{ category: 'photos', appIds: ['photos'], tools: photosTools },
|
|
@@ -88,6 +95,27 @@ export const TOOL_REGISTRY = [
|
|
|
88
95
|
{ category: 'user_status', appIds: ['user_status'], tools: userStatusTools },
|
|
89
96
|
{ category: 'notifications', appIds: ['notifications'], tools: notificationsTools },
|
|
90
97
|
{ category: 'activity', appIds: ['activity'], tools: activityTools },
|
|
98
|
+
{ category: 'announcements', appIds: ['announcementcenter'], tools: announcementTools },
|
|
99
|
+
{ category: 'registration', appIds: ['registration'], tools: registrationTools },
|
|
100
|
+
{
|
|
101
|
+
category: 'terms_of_service',
|
|
102
|
+
appIds: ['terms_of_service'],
|
|
103
|
+
tools: termsOfServiceTools,
|
|
104
|
+
},
|
|
105
|
+
{ category: 'recommendations', appIds: ['recommendations'], tools: recommendationsTools },
|
|
106
|
+
{
|
|
107
|
+
category: 'social_sharing',
|
|
108
|
+
appIds: [
|
|
109
|
+
'socialsharing_email',
|
|
110
|
+
'socialsharing_twitter',
|
|
111
|
+
'socialsharing_facebook',
|
|
112
|
+
'socialsharing_telegram',
|
|
113
|
+
'socialsharing_whatsapp',
|
|
114
|
+
'socialsharing_bluesky',
|
|
115
|
+
'socialsharing_diaspora',
|
|
116
|
+
],
|
|
117
|
+
tools: socialSharingTools,
|
|
118
|
+
},
|
|
91
119
|
];
|
|
92
120
|
const ALL_CATEGORIES = new Set(TOOL_REGISTRY.map((e) => e.category));
|
|
93
121
|
/**
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { fetchOCS } from '../../client/ocs.js';
|
|
4
|
+
function formatTime(ts) {
|
|
5
|
+
if (!ts)
|
|
6
|
+
return '';
|
|
7
|
+
return new Date(ts * 1000).toISOString();
|
|
8
|
+
}
|
|
9
|
+
function formatAnnouncements(items) {
|
|
10
|
+
return items
|
|
11
|
+
.map((a) => {
|
|
12
|
+
const time = a.time ? ` (${formatTime(a.time)})` : '';
|
|
13
|
+
const schedule = a.schedule_time ? `\n scheduled: ${formatTime(a.schedule_time)}` : '';
|
|
14
|
+
const del = a.delete_time ? `\n deletes: ${formatTime(a.delete_time)}` : '';
|
|
15
|
+
return `- #${a.id} [${a.author}] ${a.subject}${time}${schedule}${del}`;
|
|
16
|
+
})
|
|
17
|
+
.join('\n');
|
|
18
|
+
}
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
// list_announcements
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
export const listAnnouncementsTool = {
|
|
23
|
+
name: 'list_announcements',
|
|
24
|
+
description: 'List announcements from the Nextcloud Announcement Center (org-wide notices such as ' +
|
|
25
|
+
'maintenance windows, events, or news). Returns newest first.',
|
|
26
|
+
inputSchema: z.object({
|
|
27
|
+
offset: z
|
|
28
|
+
.number()
|
|
29
|
+
.optional()
|
|
30
|
+
.describe('Return announcements before this offset (for pagination, default 0)'),
|
|
31
|
+
}),
|
|
32
|
+
handler: async (args) => {
|
|
33
|
+
try {
|
|
34
|
+
const queryParams = {
|
|
35
|
+
offset: String(args.offset ?? 0),
|
|
36
|
+
};
|
|
37
|
+
const result = await fetchOCS('/ocs/v2.php/apps/announcementcenter/api/v1/announcements', { queryParams });
|
|
38
|
+
const items = result.ocs.data;
|
|
39
|
+
if (items.length === 0) {
|
|
40
|
+
return {
|
|
41
|
+
content: [{ type: 'text', text: 'No announcements.' }],
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
const lastId = items[items.length - 1].id;
|
|
45
|
+
return {
|
|
46
|
+
content: [
|
|
47
|
+
{
|
|
48
|
+
type: 'text',
|
|
49
|
+
text: `Announcements (${items.length}):\n${formatAnnouncements(items)}\n\n` +
|
|
50
|
+
`Last id: ${lastId} (pass as 'offset' to page further).`,
|
|
51
|
+
},
|
|
52
|
+
],
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
catch (error) {
|
|
56
|
+
return {
|
|
57
|
+
content: [
|
|
58
|
+
{
|
|
59
|
+
type: 'text',
|
|
60
|
+
text: `Error listing announcements: ${error instanceof Error ? error.message : String(error)}`,
|
|
61
|
+
},
|
|
62
|
+
],
|
|
63
|
+
isError: true,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
// ---------------------------------------------------------------------------
|
|
69
|
+
// create_announcement
|
|
70
|
+
// ---------------------------------------------------------------------------
|
|
71
|
+
export const createAnnouncementTool = {
|
|
72
|
+
name: 'create_announcement',
|
|
73
|
+
description: 'Create a new announcement in the Announcement Center. This is visible to all or ' +
|
|
74
|
+
'selected groups and can trigger notifications/emails — use deliberately. Requires the ' +
|
|
75
|
+
'configured Nextcloud user to be an admin.',
|
|
76
|
+
inputSchema: z.object({
|
|
77
|
+
subject: z.string().describe('The announcement title (kept short)'),
|
|
78
|
+
message: z.string().describe('The announcement body (Markdown supported)'),
|
|
79
|
+
plainMessage: z
|
|
80
|
+
.string()
|
|
81
|
+
.optional()
|
|
82
|
+
.describe('Plain-text version of the message (defaults to message)'),
|
|
83
|
+
groups: z
|
|
84
|
+
.array(z.string())
|
|
85
|
+
.optional()
|
|
86
|
+
.describe("Group IDs to target, or ['everyone'] for all users (default)"),
|
|
87
|
+
activities: z.boolean().optional().describe('Publish to the activity feed (default true)'),
|
|
88
|
+
notifications: z.boolean().optional().describe('Send notifications (default true)'),
|
|
89
|
+
emails: z.boolean().optional().describe('Send emails (default false)'),
|
|
90
|
+
comments: z.boolean().optional().describe('Allow comments (default true)'),
|
|
91
|
+
scheduleTime: z
|
|
92
|
+
.number()
|
|
93
|
+
.optional()
|
|
94
|
+
.describe('Unix timestamp (seconds) to publish the announcement later'),
|
|
95
|
+
deleteTime: z
|
|
96
|
+
.number()
|
|
97
|
+
.optional()
|
|
98
|
+
.describe('Unix timestamp (seconds) to automatically delete the announcement'),
|
|
99
|
+
}),
|
|
100
|
+
handler: async (args) => {
|
|
101
|
+
try {
|
|
102
|
+
const jsonBody = {
|
|
103
|
+
subject: args.subject,
|
|
104
|
+
message: args.message,
|
|
105
|
+
plainMessage: args.plainMessage ?? args.message,
|
|
106
|
+
groups: args.groups ?? ['everyone'],
|
|
107
|
+
activities: args.activities ?? true,
|
|
108
|
+
notifications: args.notifications ?? true,
|
|
109
|
+
emails: args.emails ?? false,
|
|
110
|
+
comments: args.comments ?? true,
|
|
111
|
+
};
|
|
112
|
+
if (args.scheduleTime !== undefined)
|
|
113
|
+
jsonBody.scheduleTime = args.scheduleTime;
|
|
114
|
+
if (args.deleteTime !== undefined)
|
|
115
|
+
jsonBody.deleteTime = args.deleteTime;
|
|
116
|
+
const result = await fetchOCS('/ocs/v2.php/apps/announcementcenter/api/v1/announcements', { method: 'POST', jsonBody });
|
|
117
|
+
const created = result.ocs.data;
|
|
118
|
+
return {
|
|
119
|
+
content: [
|
|
120
|
+
{
|
|
121
|
+
type: 'text',
|
|
122
|
+
text: `Created announcement #${created.id}: "${created.subject}"`,
|
|
123
|
+
},
|
|
124
|
+
],
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
catch (error) {
|
|
128
|
+
return {
|
|
129
|
+
content: [
|
|
130
|
+
{
|
|
131
|
+
type: 'text',
|
|
132
|
+
text: `Error creating announcement: ${error instanceof Error ? error.message : String(error)}`,
|
|
133
|
+
},
|
|
134
|
+
],
|
|
135
|
+
isError: true,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
},
|
|
139
|
+
};
|
|
140
|
+
// ---------------------------------------------------------------------------
|
|
141
|
+
// delete_announcement
|
|
142
|
+
// ---------------------------------------------------------------------------
|
|
143
|
+
export const deleteAnnouncementTool = {
|
|
144
|
+
name: 'delete_announcement',
|
|
145
|
+
description: 'Delete an announcement by its ID. Requires the configured Nextcloud user to be an admin.',
|
|
146
|
+
inputSchema: z.object({
|
|
147
|
+
id: z.number().describe('The announcement ID to delete'),
|
|
148
|
+
}),
|
|
149
|
+
handler: async (args) => {
|
|
150
|
+
try {
|
|
151
|
+
await fetchOCS(`/ocs/v2.php/apps/announcementcenter/api/v1/announcements/${args.id}`, {
|
|
152
|
+
method: 'DELETE',
|
|
153
|
+
});
|
|
154
|
+
return {
|
|
155
|
+
content: [{ type: 'text', text: `Deleted announcement #${args.id}.` }],
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
catch (error) {
|
|
159
|
+
return {
|
|
160
|
+
content: [
|
|
161
|
+
{
|
|
162
|
+
type: 'text',
|
|
163
|
+
text: `Error deleting announcement: ${error instanceof Error ? error.message : String(error)}`,
|
|
164
|
+
},
|
|
165
|
+
],
|
|
166
|
+
isError: true,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
},
|
|
170
|
+
};
|
|
171
|
+
// ---------------------------------------------------------------------------
|
|
172
|
+
// Export
|
|
173
|
+
// ---------------------------------------------------------------------------
|
|
174
|
+
export const announcementTools = [
|
|
175
|
+
listAnnouncementsTool,
|
|
176
|
+
createAnnouncementTool,
|
|
177
|
+
deleteAnnouncementTool,
|
|
178
|
+
];
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { fetchPassmanAPI, } from '../../client/passman.js';
|
|
4
|
+
import { handleAppError } from '../error-utils.js';
|
|
5
|
+
/**
|
|
6
|
+
* Nextcloud Passman App Tools (password manager)
|
|
7
|
+
* Uses the Passman REST API v2 (/index.php/apps/passman/api/v2).
|
|
8
|
+
*
|
|
9
|
+
* IMPORTANT: Passman is zero-knowledge / end-to-end encrypted. Credential
|
|
10
|
+
* secrets (password, username, url, email, description, custom fields, OTP,
|
|
11
|
+
* tags, files) are encrypted client-side (SJCL AES-256) with a vault password
|
|
12
|
+
* that never reaches the server. This integration therefore exposes vault and
|
|
13
|
+
* credential *metadata only* (notably the plaintext `label`) and cannot decrypt
|
|
14
|
+
* or return any secret value.
|
|
15
|
+
*/
|
|
16
|
+
const passmanStatusMap = {
|
|
17
|
+
404: 'Not found.',
|
|
18
|
+
403: 'Permission denied.',
|
|
19
|
+
400: (e) => `Bad request: ${e.responseBody}`,
|
|
20
|
+
};
|
|
21
|
+
const E2E_NOTE = 'Note: secret values (password, username, url, etc.) are end-to-end encrypted and cannot be retrieved by this server.';
|
|
22
|
+
function formatVault(vault) {
|
|
23
|
+
const count = vault.credentials?.length ?? undefined;
|
|
24
|
+
const parts = [`guid: ${vault.guid}`];
|
|
25
|
+
if (count !== undefined)
|
|
26
|
+
parts.push(`${count} credentials`);
|
|
27
|
+
parts.push(`created: ${new Date(vault.created * 1000).toISOString()}`);
|
|
28
|
+
if (vault.last_access) {
|
|
29
|
+
parts.push(`last access: ${new Date(vault.last_access * 1000).toISOString()}`);
|
|
30
|
+
}
|
|
31
|
+
return `${vault.name} (${parts.join(', ')})`;
|
|
32
|
+
}
|
|
33
|
+
function hasValue(v) {
|
|
34
|
+
if (v === undefined || v === null || v === '')
|
|
35
|
+
return false;
|
|
36
|
+
if (Array.isArray(v))
|
|
37
|
+
return v.length > 0;
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
function formatCredential(cred) {
|
|
41
|
+
const flags = [];
|
|
42
|
+
if (hasValue(cred.url))
|
|
43
|
+
flags.push('has url');
|
|
44
|
+
if (hasValue(cred.tags))
|
|
45
|
+
flags.push('tagged');
|
|
46
|
+
if (cred.hidden)
|
|
47
|
+
flags.push('hidden');
|
|
48
|
+
if (cred.compromised)
|
|
49
|
+
flags.push('compromised');
|
|
50
|
+
if (cred.expire_time) {
|
|
51
|
+
flags.push(`expires: ${new Date(cred.expire_time * 1000).toISOString()}`);
|
|
52
|
+
}
|
|
53
|
+
const meta = [`guid: ${cred.guid}`, ...flags].join(', ');
|
|
54
|
+
return `[${cred.credential_id}] ${cred.label} (${meta})`;
|
|
55
|
+
}
|
|
56
|
+
export const listVaultsTool = {
|
|
57
|
+
name: 'passman_list_vaults',
|
|
58
|
+
description: `List all Passman password vaults. Returns name, guid, creation/last-access time and credential count. ${E2E_NOTE}`,
|
|
59
|
+
inputSchema: z.object({}),
|
|
60
|
+
handler: async () => {
|
|
61
|
+
try {
|
|
62
|
+
const vaults = await fetchPassmanAPI('/vaults');
|
|
63
|
+
if (!vaults || vaults.length === 0) {
|
|
64
|
+
return { content: [{ type: 'text', text: 'No vaults found.' }] };
|
|
65
|
+
}
|
|
66
|
+
return {
|
|
67
|
+
content: [
|
|
68
|
+
{
|
|
69
|
+
type: 'text',
|
|
70
|
+
text: `Vaults (${vaults.length}):\n\n${vaults.map(formatVault).join('\n')}\n\n${E2E_NOTE}`,
|
|
71
|
+
},
|
|
72
|
+
],
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
return handleAppError(error, 'Error listing vaults', passmanStatusMap);
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
export const listCredentialsTool = {
|
|
81
|
+
name: 'passman_list_credentials',
|
|
82
|
+
description: `List credentials (metadata only) in a Passman vault. Returns each credential's label, id, guid and non-secret flags. ${E2E_NOTE}`,
|
|
83
|
+
inputSchema: z.object({
|
|
84
|
+
vault_guid: z.string().describe('Vault GUID (from passman_list_vaults)'),
|
|
85
|
+
}),
|
|
86
|
+
handler: async (args) => {
|
|
87
|
+
try {
|
|
88
|
+
const vault = await fetchPassmanAPI(`/vaults/${encodeURIComponent(args.vault_guid)}`);
|
|
89
|
+
const creds = vault.credentials ?? [];
|
|
90
|
+
if (creds.length === 0) {
|
|
91
|
+
return { content: [{ type: 'text', text: 'No credentials in this vault.' }] };
|
|
92
|
+
}
|
|
93
|
+
return {
|
|
94
|
+
content: [
|
|
95
|
+
{
|
|
96
|
+
type: 'text',
|
|
97
|
+
text: `Credentials in "${vault.name}" (${creds.length}):\n\n${creds
|
|
98
|
+
.map(formatCredential)
|
|
99
|
+
.join('\n')}\n\n${E2E_NOTE}`,
|
|
100
|
+
},
|
|
101
|
+
],
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
catch (error) {
|
|
105
|
+
return handleAppError(error, 'Error listing credentials', passmanStatusMap);
|
|
106
|
+
}
|
|
107
|
+
},
|
|
108
|
+
};
|
|
109
|
+
export const getCredentialInfoTool = {
|
|
110
|
+
name: 'passman_get_credential_info',
|
|
111
|
+
description: `Get non-secret metadata for a single Passman credential (label, id, timestamps, flags). ${E2E_NOTE}`,
|
|
112
|
+
inputSchema: z.object({
|
|
113
|
+
vault_guid: z.string().describe('Vault GUID (from passman_list_vaults)'),
|
|
114
|
+
credential_guid: z.string().describe('Credential GUID (from passman_list_credentials)'),
|
|
115
|
+
}),
|
|
116
|
+
handler: async (args) => {
|
|
117
|
+
try {
|
|
118
|
+
const vault = await fetchPassmanAPI(`/vaults/${encodeURIComponent(args.vault_guid)}`);
|
|
119
|
+
const cred = (vault.credentials ?? []).find((c) => c.guid === args.credential_guid);
|
|
120
|
+
if (!cred) {
|
|
121
|
+
return {
|
|
122
|
+
content: [{ type: 'text', text: 'Credential not found in this vault.' }],
|
|
123
|
+
isError: true,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
const lines = [
|
|
127
|
+
`# ${cred.label}`,
|
|
128
|
+
'',
|
|
129
|
+
`ID: ${cred.credential_id}`,
|
|
130
|
+
`GUID: ${cred.guid}`,
|
|
131
|
+
`Vault: ${vault.name}`,
|
|
132
|
+
`Created: ${new Date(cred.created * 1000).toISOString()}`,
|
|
133
|
+
`Changed: ${new Date(cred.changed * 1000).toISOString()}`,
|
|
134
|
+
`Has URL: ${hasValue(cred.url)}`,
|
|
135
|
+
`Tagged: ${hasValue(cred.tags)}`,
|
|
136
|
+
`Hidden: ${Boolean(cred.hidden)}`,
|
|
137
|
+
`Compromised: ${Boolean(cred.compromised)}`,
|
|
138
|
+
];
|
|
139
|
+
if (cred.expire_time) {
|
|
140
|
+
lines.push(`Expires: ${new Date(cred.expire_time * 1000).toISOString()}`);
|
|
141
|
+
}
|
|
142
|
+
lines.push('', E2E_NOTE);
|
|
143
|
+
return { content: [{ type: 'text', text: lines.join('\n') }] };
|
|
144
|
+
}
|
|
145
|
+
catch (error) {
|
|
146
|
+
return handleAppError(error, 'Error getting credential info', passmanStatusMap);
|
|
147
|
+
}
|
|
148
|
+
},
|
|
149
|
+
};
|
|
150
|
+
export const passmanTools = [listVaultsTool, listCredentialsTool, getCredentialInfoTool];
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { fetchOCS } from '../../client/ocs.js';
|
|
4
|
+
function formatTime(ts) {
|
|
5
|
+
if (!ts)
|
|
6
|
+
return '';
|
|
7
|
+
return new Date(ts * 1000).toISOString();
|
|
8
|
+
}
|
|
9
|
+
function formatRecommendations(items) {
|
|
10
|
+
return items
|
|
11
|
+
.map((f) => {
|
|
12
|
+
const dir = f.directory && f.directory !== '/' ? f.directory.replace(/\/$/, '') : '';
|
|
13
|
+
const path = `${dir}/${f.name}`;
|
|
14
|
+
const time = f.timestamp ? ` (${formatTime(f.timestamp)})` : '';
|
|
15
|
+
const reason = f.reason ? `\n reason: ${f.reason}` : '';
|
|
16
|
+
return `- ${path}${time}${reason}`;
|
|
17
|
+
})
|
|
18
|
+
.join('\n');
|
|
19
|
+
}
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
// list_recommendations
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
export const listRecommendationsTool = {
|
|
24
|
+
name: 'list_recommendations',
|
|
25
|
+
description: 'List files Nextcloud recommends for the configured user (e.g. recently or frequently ' +
|
|
26
|
+
'accessed, recently shared, or files in active folders), including the reason each file ' +
|
|
27
|
+
"is recommended. Respects the user's recommendations setting.",
|
|
28
|
+
inputSchema: z.object({}),
|
|
29
|
+
handler: async () => {
|
|
30
|
+
try {
|
|
31
|
+
const result = await fetchOCS('/ocs/v2.php/apps/recommendations/api/v1/recommendations');
|
|
32
|
+
const data = result.ocs.data;
|
|
33
|
+
const items = data.recommendations ?? [];
|
|
34
|
+
if (!data.enabled && items.length === 0) {
|
|
35
|
+
return {
|
|
36
|
+
content: [
|
|
37
|
+
{
|
|
38
|
+
type: 'text',
|
|
39
|
+
text: 'Recommendations are disabled for this user.',
|
|
40
|
+
},
|
|
41
|
+
],
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
if (items.length === 0) {
|
|
45
|
+
return {
|
|
46
|
+
content: [{ type: 'text', text: 'No recommended files.' }],
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
return {
|
|
50
|
+
content: [
|
|
51
|
+
{
|
|
52
|
+
type: 'text',
|
|
53
|
+
text: `Recommended files (${items.length}):\n${formatRecommendations(items)}`,
|
|
54
|
+
},
|
|
55
|
+
],
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
return {
|
|
60
|
+
content: [
|
|
61
|
+
{
|
|
62
|
+
type: 'text',
|
|
63
|
+
text: `Error listing recommendations: ${error instanceof Error ? error.message : String(error)}`,
|
|
64
|
+
},
|
|
65
|
+
],
|
|
66
|
+
isError: true,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
// ---------------------------------------------------------------------------
|
|
72
|
+
// Export
|
|
73
|
+
// ---------------------------------------------------------------------------
|
|
74
|
+
export const recommendationsTools = [listRecommendationsTool];
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { fetchOCS } from '../../client/ocs.js';
|
|
4
|
+
/**
|
|
5
|
+
* Nextcloud Registration Tools
|
|
6
|
+
*
|
|
7
|
+
* The Registration app (https://github.com/nextcloud/registration) lets visitors
|
|
8
|
+
* self-register accounts via an email-verification flow. It exposes no API of its
|
|
9
|
+
* own — all admin behaviour is driven by app config keys stored under the
|
|
10
|
+
* `registration` app id. These tools read and write those keys through core's
|
|
11
|
+
* provisioning_api appconfig OCS endpoints.
|
|
12
|
+
*
|
|
13
|
+
* Pending registrations are not exposed by any API. When `admin_approval_required`
|
|
14
|
+
* is enabled, registrants become disabled Nextcloud users — approve or reject them
|
|
15
|
+
* with the standard user tools (enable_user / delete_user).
|
|
16
|
+
*/
|
|
17
|
+
const APPCONFIG_BASE = '/ocs/v2.php/apps/provisioning_api/api/v1/config/apps/registration';
|
|
18
|
+
/** Known Registration app config keys, with short descriptions. */
|
|
19
|
+
const REGISTRATION_KEYS = {
|
|
20
|
+
allowed_domains: 'Email domains allowed (or, with domains_is_blocklist, blocked) for registration (JSON array string)',
|
|
21
|
+
domains_is_blocklist: 'Treat allowed_domains as a blocklist instead of an allowlist (yes/no)',
|
|
22
|
+
show_domains: 'Show the email domain list to users (yes/no)',
|
|
23
|
+
admin_approval_required: 'Newly registered users must be validated by an admin (yes/no)',
|
|
24
|
+
registered_user_group: 'Group id newly registered users are added to',
|
|
25
|
+
email_is_optional: 'Email address is optional during registration (yes/no)',
|
|
26
|
+
email_is_login: 'Force the email address as the user id / login (yes/no)',
|
|
27
|
+
disable_email_verification: 'Skip the email verification step (yes/no)',
|
|
28
|
+
email_verification_hint: 'Text embedded in the verification email',
|
|
29
|
+
additional_hint: 'Text displayed on the account creation form',
|
|
30
|
+
username_policy_regex: 'Optional regex the chosen username must match',
|
|
31
|
+
show_fullname: 'Show the full name field on the registration form (yes/no)',
|
|
32
|
+
enforce_fullname: 'Make the full name field mandatory (yes/no)',
|
|
33
|
+
show_phone: 'Show the phone field on the registration form (yes/no)',
|
|
34
|
+
enforce_phone: 'Make the phone field mandatory (yes/no)',
|
|
35
|
+
};
|
|
36
|
+
const KNOWN_KEYS = Object.keys(REGISTRATION_KEYS);
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
// get_registration_settings
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
40
|
+
export const getRegistrationSettingsTool = {
|
|
41
|
+
name: 'get_registration_settings',
|
|
42
|
+
description: 'Read the Nextcloud Registration app settings (self-service signup configuration), such as ' +
|
|
43
|
+
'allowed email domains, whether admin approval is required, and the default group for new ' +
|
|
44
|
+
'users. Requires the configured Nextcloud user to be an admin.',
|
|
45
|
+
inputSchema: z.object({}),
|
|
46
|
+
handler: async () => {
|
|
47
|
+
try {
|
|
48
|
+
const entries = await Promise.all(KNOWN_KEYS.map(async (key) => {
|
|
49
|
+
const result = await fetchOCS(`${APPCONFIG_BASE}/${key}`);
|
|
50
|
+
return [key, result.ocs.data];
|
|
51
|
+
}));
|
|
52
|
+
const text = entries
|
|
53
|
+
.map(([key, value]) => `- ${key}: ${value === '' ? '(default)' : value}`)
|
|
54
|
+
.join('\n');
|
|
55
|
+
return {
|
|
56
|
+
content: [{ type: 'text', text: `Registration settings:\n${text}` }],
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
return {
|
|
61
|
+
content: [
|
|
62
|
+
{
|
|
63
|
+
type: 'text',
|
|
64
|
+
text: `Error reading registration settings: ${error instanceof Error ? error.message : String(error)}`,
|
|
65
|
+
},
|
|
66
|
+
],
|
|
67
|
+
isError: true,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
// ---------------------------------------------------------------------------
|
|
73
|
+
// update_registration_settings
|
|
74
|
+
// ---------------------------------------------------------------------------
|
|
75
|
+
export const updateRegistrationSettingsTool = {
|
|
76
|
+
name: 'update_registration_settings',
|
|
77
|
+
description: 'Update one or more Nextcloud Registration app settings. Boolean settings use the strings ' +
|
|
78
|
+
"'yes'/'no'; allowed_domains takes a JSON array string (e.g. '[\"example.com\"]'). Requires " +
|
|
79
|
+
'the configured Nextcloud user to be an admin.',
|
|
80
|
+
inputSchema: z.object({
|
|
81
|
+
settings: z
|
|
82
|
+
.array(z.object({
|
|
83
|
+
key: z
|
|
84
|
+
.enum(KNOWN_KEYS)
|
|
85
|
+
.describe('The Registration config key to set'),
|
|
86
|
+
value: z.string().describe('The value to store (boolean keys: "yes"/"no")'),
|
|
87
|
+
}))
|
|
88
|
+
.min(1)
|
|
89
|
+
.describe('One or more key/value pairs to update'),
|
|
90
|
+
}),
|
|
91
|
+
handler: async (args) => {
|
|
92
|
+
try {
|
|
93
|
+
const updated = [];
|
|
94
|
+
for (const { key, value } of args.settings) {
|
|
95
|
+
await fetchOCS(`${APPCONFIG_BASE}/${key}`, {
|
|
96
|
+
method: 'POST',
|
|
97
|
+
body: { value },
|
|
98
|
+
});
|
|
99
|
+
updated.push(`${key} = ${value}`);
|
|
100
|
+
}
|
|
101
|
+
return {
|
|
102
|
+
content: [
|
|
103
|
+
{
|
|
104
|
+
type: 'text',
|
|
105
|
+
text: `Updated registration settings:\n${updated.map((u) => `- ${u}`).join('\n')}`,
|
|
106
|
+
},
|
|
107
|
+
],
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
catch (error) {
|
|
111
|
+
return {
|
|
112
|
+
content: [
|
|
113
|
+
{
|
|
114
|
+
type: 'text',
|
|
115
|
+
text: `Error updating registration settings: ${error instanceof Error ? error.message : String(error)}`,
|
|
116
|
+
},
|
|
117
|
+
],
|
|
118
|
+
isError: true,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
},
|
|
122
|
+
};
|
|
123
|
+
// ---------------------------------------------------------------------------
|
|
124
|
+
// reset_registration_setting
|
|
125
|
+
// ---------------------------------------------------------------------------
|
|
126
|
+
export const resetRegistrationSettingTool = {
|
|
127
|
+
name: 'reset_registration_setting',
|
|
128
|
+
description: 'Reset a Nextcloud Registration app setting to its default by deleting the stored value. ' +
|
|
129
|
+
'Requires the configured Nextcloud user to be an admin.',
|
|
130
|
+
inputSchema: z.object({
|
|
131
|
+
key: z
|
|
132
|
+
.enum(KNOWN_KEYS)
|
|
133
|
+
.describe('The Registration config key to reset to its default'),
|
|
134
|
+
}),
|
|
135
|
+
handler: async (args) => {
|
|
136
|
+
try {
|
|
137
|
+
await fetchOCS(`${APPCONFIG_BASE}/${args.key}`, { method: 'DELETE' });
|
|
138
|
+
return {
|
|
139
|
+
content: [
|
|
140
|
+
{ type: 'text', text: `Reset registration setting '${args.key}' to default.` },
|
|
141
|
+
],
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
catch (error) {
|
|
145
|
+
return {
|
|
146
|
+
content: [
|
|
147
|
+
{
|
|
148
|
+
type: 'text',
|
|
149
|
+
text: `Error resetting registration setting: ${error instanceof Error ? error.message : String(error)}`,
|
|
150
|
+
},
|
|
151
|
+
],
|
|
152
|
+
isError: true,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
},
|
|
156
|
+
};
|
|
157
|
+
// ---------------------------------------------------------------------------
|
|
158
|
+
// Export
|
|
159
|
+
// ---------------------------------------------------------------------------
|
|
160
|
+
export const registrationTools = [
|
|
161
|
+
getRegistrationSettingsTool,
|
|
162
|
+
updateRegistrationSettingsTool,
|
|
163
|
+
resetRegistrationSettingTool,
|
|
164
|
+
];
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { fetchOCS } from '../../client/ocs.js';
|
|
4
|
+
import { getNextcloudConfig } from '../types.js';
|
|
5
|
+
import { handleAppError } from '../error-utils.js';
|
|
6
|
+
const SHARE_TYPE_PUBLIC_LINK = 3;
|
|
7
|
+
/** Default share message used by the upstream apps. */
|
|
8
|
+
const DEFAULT_MESSAGE = 'I shared a file with you';
|
|
9
|
+
const NETWORKS = [
|
|
10
|
+
{
|
|
11
|
+
key: 'email',
|
|
12
|
+
appId: 'socialsharing_email',
|
|
13
|
+
build: (link, msg) => `mailto:?subject=${msg}&body=${encodeURIComponent(link)}`,
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
key: 'twitter',
|
|
17
|
+
appId: 'socialsharing_twitter',
|
|
18
|
+
build: (link) => `https://twitter.com/intent/tweet?url=${encodeURIComponent(link)}`,
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
key: 'facebook',
|
|
22
|
+
appId: 'socialsharing_facebook',
|
|
23
|
+
build: (link) => `https://www.facebook.com/sharer/sharer.php?u=${encodeURIComponent(link)}`,
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
key: 'telegram',
|
|
27
|
+
appId: 'socialsharing_telegram',
|
|
28
|
+
build: (link, msg) => `https://telegram.me/share/url?url=${encodeURIComponent(link)}&text=${msg}`,
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
key: 'whatsapp',
|
|
32
|
+
appId: 'socialsharing_whatsapp',
|
|
33
|
+
build: (link, msg) => `whatsapp://send?text=${msg}:%0A%0A${encodeURIComponent(link)}`,
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
key: 'bluesky',
|
|
37
|
+
appId: 'socialsharing_bluesky',
|
|
38
|
+
build: (link, msg) => `https://bsky.app/intent/compose?text=${msg}:%0A%0A${encodeURIComponent(link)}`,
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
key: 'diaspora',
|
|
42
|
+
appId: 'socialsharing_diaspora',
|
|
43
|
+
build: (link) => `https://share.diasporafoundation.org/?url=${encodeURIComponent(link)}`,
|
|
44
|
+
},
|
|
45
|
+
];
|
|
46
|
+
/** Fetch the set of enabled Nextcloud app ids. */
|
|
47
|
+
async function getEnabledApps() {
|
|
48
|
+
const result = await fetchOCS('/ocs/v2.php/cloud/apps', {
|
|
49
|
+
queryParams: { filter: 'enabled' },
|
|
50
|
+
});
|
|
51
|
+
return new Set(result.ocs.data.apps);
|
|
52
|
+
}
|
|
53
|
+
/** Resolve a public-link share by id or path. */
|
|
54
|
+
async function resolvePublicShare(args) {
|
|
55
|
+
if (args.share_id !== undefined) {
|
|
56
|
+
const result = await fetchOCS(`/ocs/v2.php/apps/files_sharing/api/v1/shares/${args.share_id}`);
|
|
57
|
+
// The single-share endpoint returns an array with one entry.
|
|
58
|
+
const data = result.ocs.data;
|
|
59
|
+
const share = Array.isArray(data) ? data[0] : data;
|
|
60
|
+
if (!share)
|
|
61
|
+
throw new Error(`Share ${args.share_id} not found`);
|
|
62
|
+
return share;
|
|
63
|
+
}
|
|
64
|
+
const result = await fetchOCS('/ocs/v2.php/apps/files_sharing/api/v1/shares', {
|
|
65
|
+
queryParams: { path: args.path },
|
|
66
|
+
});
|
|
67
|
+
const link = result.ocs.data.find((s) => s.share_type === SHARE_TYPE_PUBLIC_LINK && s.token);
|
|
68
|
+
if (!link) {
|
|
69
|
+
throw new Error(`No public link share found for path "${args.path}". Create one first.`);
|
|
70
|
+
}
|
|
71
|
+
return link;
|
|
72
|
+
}
|
|
73
|
+
export const generateSocialShareLinksTool = {
|
|
74
|
+
name: 'generate_social_share_links',
|
|
75
|
+
description: 'Generate social-network share URLs (email, X/Twitter, Facebook, Telegram, WhatsApp, ' +
|
|
76
|
+
'Bluesky, Diaspora) for an existing Nextcloud public-link share. Only networks whose ' +
|
|
77
|
+
'socialsharing_<network> app is enabled are returned. Provide either share_id or path.',
|
|
78
|
+
inputSchema: z.object({
|
|
79
|
+
share_id: z.number().optional().describe('ID of an existing public-link share'),
|
|
80
|
+
path: z
|
|
81
|
+
.string()
|
|
82
|
+
.optional()
|
|
83
|
+
.describe('File/folder path; its public-link share is used (must already exist)'),
|
|
84
|
+
networks: z
|
|
85
|
+
.array(z.string())
|
|
86
|
+
.optional()
|
|
87
|
+
.describe('Optional filter, e.g. ["twitter","email"]. Defaults to all enabled networks.'),
|
|
88
|
+
}),
|
|
89
|
+
handler: async (args) => {
|
|
90
|
+
if (args.share_id === undefined && !args.path) {
|
|
91
|
+
return {
|
|
92
|
+
content: [{ type: 'text', text: 'Provide either share_id or path.' }],
|
|
93
|
+
isError: true,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
try {
|
|
97
|
+
const share = await resolvePublicShare(args);
|
|
98
|
+
if (share.share_type !== SHARE_TYPE_PUBLIC_LINK || !share.token) {
|
|
99
|
+
return {
|
|
100
|
+
content: [
|
|
101
|
+
{
|
|
102
|
+
type: 'text',
|
|
103
|
+
text: 'The referenced share is not a public link (no shareable token).',
|
|
104
|
+
},
|
|
105
|
+
],
|
|
106
|
+
isError: true,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
const { url } = getNextcloudConfig();
|
|
110
|
+
const link = share.url || `${url}/s/${share.token}`;
|
|
111
|
+
const message = encodeURIComponent(DEFAULT_MESSAGE);
|
|
112
|
+
const filter = args.networks ? new Set(args.networks.map((n) => n.toLowerCase())) : null;
|
|
113
|
+
const enabledApps = await getEnabledApps();
|
|
114
|
+
const lines = NETWORKS.filter((n) => enabledApps.has(n.appId))
|
|
115
|
+
.filter((n) => !filter || filter.has(n.key))
|
|
116
|
+
.map((n) => `${n.key}: ${n.build(link, message)}`);
|
|
117
|
+
if (lines.length === 0) {
|
|
118
|
+
return {
|
|
119
|
+
content: [
|
|
120
|
+
{
|
|
121
|
+
type: 'text',
|
|
122
|
+
text: 'No matching social sharing networks are available. Install/enable a ' +
|
|
123
|
+
'socialsharing_<network> app (or adjust the networks filter).',
|
|
124
|
+
},
|
|
125
|
+
],
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
return {
|
|
129
|
+
content: [
|
|
130
|
+
{
|
|
131
|
+
type: 'text',
|
|
132
|
+
text: `Social share links for ${link}:\n${lines.join('\n')}`,
|
|
133
|
+
},
|
|
134
|
+
],
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
catch (error) {
|
|
138
|
+
return handleAppError(error, 'Error generating social share links');
|
|
139
|
+
}
|
|
140
|
+
},
|
|
141
|
+
};
|
|
142
|
+
export const socialSharingTools = [generateSocialShareLinksTool];
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { fetchOCS } from '../../client/ocs.js';
|
|
4
|
+
/**
|
|
5
|
+
* Nextcloud Terms of Service Tools
|
|
6
|
+
*
|
|
7
|
+
* The Terms of Service app (https://github.com/nextcloud/terms_of_service) lets admins
|
|
8
|
+
* publish per-country / per-language terms that users must accept. It exposes its own OCS
|
|
9
|
+
* API under `/ocs/v2.php/apps/terms_of_service`, so most tools call those endpoints
|
|
10
|
+
* directly.
|
|
11
|
+
*
|
|
12
|
+
* Two enforcement flags (`tos_for_users`, `tos_on_public_shares`) are plain app config
|
|
13
|
+
* keys: they are read via the admin form endpoint and written through core's
|
|
14
|
+
* provisioning_api appconfig endpoints.
|
|
15
|
+
*/
|
|
16
|
+
const OCS_BASE = '/ocs/v2.php/apps/terms_of_service';
|
|
17
|
+
const APPCONFIG_BASE = '/ocs/v2.php/apps/provisioning_api/api/v1/config/apps/terms_of_service';
|
|
18
|
+
function preview(body) {
|
|
19
|
+
const oneLine = body.replace(/\s+/g, ' ').trim();
|
|
20
|
+
return oneLine.length > 80 ? `${oneLine.slice(0, 80)}…` : oneLine;
|
|
21
|
+
}
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
// get_terms_of_service
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
export const getTermsOfServiceTool = {
|
|
26
|
+
name: 'get_terms_of_service',
|
|
27
|
+
description: 'Read the Nextcloud Terms of Service admin configuration: all published terms (by ' +
|
|
28
|
+
'country/language), the enforcement settings (tos_for_users, tos_on_public_shares), and ' +
|
|
29
|
+
'the valid country and language codes that can be used when setting terms. Requires the ' +
|
|
30
|
+
'configured Nextcloud user to be an admin.',
|
|
31
|
+
inputSchema: z.object({}),
|
|
32
|
+
handler: async () => {
|
|
33
|
+
try {
|
|
34
|
+
const result = await fetchOCS(`${OCS_BASE}/terms/admin`);
|
|
35
|
+
const data = result.ocs.data;
|
|
36
|
+
const termsText = data.terms.length === 0
|
|
37
|
+
? '(none)'
|
|
38
|
+
: data.terms
|
|
39
|
+
.map((t) => `- #${t.id} [${t.countryCode}/${t.languageCode}] ${preview(t.body)}`)
|
|
40
|
+
.join('\n');
|
|
41
|
+
const settings = [
|
|
42
|
+
`- tos_for_users: ${data.tos_for_users}`,
|
|
43
|
+
`- tos_on_public_shares: ${data.tos_on_public_shares}`,
|
|
44
|
+
].join('\n');
|
|
45
|
+
const countries = Object.keys(data.countries).join(', ');
|
|
46
|
+
const languages = Object.keys(data.languages).join(', ');
|
|
47
|
+
const text = `Terms of Service:\n${termsText}\n\n` +
|
|
48
|
+
`Settings:\n${settings}\n\n` +
|
|
49
|
+
`Valid country codes (use "--" for global): ${countries}\n` +
|
|
50
|
+
`Valid language codes: ${languages}`;
|
|
51
|
+
return {
|
|
52
|
+
content: [{ type: 'text', text }],
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
catch (error) {
|
|
56
|
+
return {
|
|
57
|
+
content: [
|
|
58
|
+
{
|
|
59
|
+
type: 'text',
|
|
60
|
+
text: `Error reading terms of service: ${error instanceof Error ? error.message : String(error)}`,
|
|
61
|
+
},
|
|
62
|
+
],
|
|
63
|
+
isError: true,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
// ---------------------------------------------------------------------------
|
|
69
|
+
// set_terms_of_service
|
|
70
|
+
// ---------------------------------------------------------------------------
|
|
71
|
+
export const setTermsOfServiceTool = {
|
|
72
|
+
name: 'set_terms_of_service',
|
|
73
|
+
description: 'Create or update the terms of service for a given country/language pair. If terms ' +
|
|
74
|
+
'already exist for that pair they are replaced, otherwise new terms are created. Use ' +
|
|
75
|
+
'get_terms_of_service to discover valid country/language codes; an invalid code returns ' +
|
|
76
|
+
'an error (HTTP 417). Requires the configured Nextcloud user to be an admin.',
|
|
77
|
+
inputSchema: z.object({
|
|
78
|
+
countryCode: z
|
|
79
|
+
.string()
|
|
80
|
+
.default('--')
|
|
81
|
+
.describe('2-letter region code, or "--" for global (default)'),
|
|
82
|
+
languageCode: z.string().describe('2-letter language code (e.g. "en")'),
|
|
83
|
+
body: z.string().describe('The terms text (markdown: headers, formatting, lists, links)'),
|
|
84
|
+
}),
|
|
85
|
+
handler: async (args) => {
|
|
86
|
+
const countryCode = args.countryCode ?? '--';
|
|
87
|
+
try {
|
|
88
|
+
const result = await fetchOCS(`${OCS_BASE}/terms`, {
|
|
89
|
+
method: 'POST',
|
|
90
|
+
body: {
|
|
91
|
+
countryCode,
|
|
92
|
+
languageCode: args.languageCode,
|
|
93
|
+
body: args.body,
|
|
94
|
+
},
|
|
95
|
+
});
|
|
96
|
+
const t = result.ocs.data;
|
|
97
|
+
return {
|
|
98
|
+
content: [
|
|
99
|
+
{
|
|
100
|
+
type: 'text',
|
|
101
|
+
text: `Saved terms #${t.id} for [${t.countryCode}/${t.languageCode}].`,
|
|
102
|
+
},
|
|
103
|
+
],
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
catch (error) {
|
|
107
|
+
return {
|
|
108
|
+
content: [
|
|
109
|
+
{
|
|
110
|
+
type: 'text',
|
|
111
|
+
text: `Error saving terms of service: ${error instanceof Error ? error.message : String(error)}`,
|
|
112
|
+
},
|
|
113
|
+
],
|
|
114
|
+
isError: true,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
// ---------------------------------------------------------------------------
|
|
120
|
+
// delete_terms_of_service
|
|
121
|
+
// ---------------------------------------------------------------------------
|
|
122
|
+
export const deleteTermsOfServiceTool = {
|
|
123
|
+
name: 'delete_terms_of_service',
|
|
124
|
+
description: 'Delete a single terms of service entry by its id (see get_terms_of_service). Requires ' +
|
|
125
|
+
'the configured Nextcloud user to be an admin.',
|
|
126
|
+
inputSchema: z.object({
|
|
127
|
+
id: z.number().describe('The terms id to delete'),
|
|
128
|
+
}),
|
|
129
|
+
handler: async (args) => {
|
|
130
|
+
try {
|
|
131
|
+
await fetchOCS(`${OCS_BASE}/terms/${args.id}`, { method: 'DELETE' });
|
|
132
|
+
return {
|
|
133
|
+
content: [{ type: 'text', text: `Deleted terms #${args.id}.` }],
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
catch (error) {
|
|
137
|
+
return {
|
|
138
|
+
content: [
|
|
139
|
+
{
|
|
140
|
+
type: 'text',
|
|
141
|
+
text: `Error deleting terms of service: ${error instanceof Error ? error.message : String(error)}`,
|
|
142
|
+
},
|
|
143
|
+
],
|
|
144
|
+
isError: true,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
},
|
|
148
|
+
};
|
|
149
|
+
// ---------------------------------------------------------------------------
|
|
150
|
+
// reset_terms_signatures
|
|
151
|
+
// ---------------------------------------------------------------------------
|
|
152
|
+
export const resetTermsSignaturesTool = {
|
|
153
|
+
name: 'reset_terms_signatures',
|
|
154
|
+
description: "Reset ALL users' terms of service signatures org-wide, forcing every user to accept " +
|
|
155
|
+
'the terms again on next login. This is destructive and cannot be undone. Requires the ' +
|
|
156
|
+
'configured Nextcloud user to be an admin.',
|
|
157
|
+
inputSchema: z.object({}),
|
|
158
|
+
handler: async () => {
|
|
159
|
+
try {
|
|
160
|
+
await fetchOCS(`${OCS_BASE}/sign`, { method: 'DELETE' });
|
|
161
|
+
return {
|
|
162
|
+
content: [
|
|
163
|
+
{
|
|
164
|
+
type: 'text',
|
|
165
|
+
text: 'Reset all terms of service signatures. All users must accept the terms again.',
|
|
166
|
+
},
|
|
167
|
+
],
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
catch (error) {
|
|
171
|
+
return {
|
|
172
|
+
content: [
|
|
173
|
+
{
|
|
174
|
+
type: 'text',
|
|
175
|
+
text: `Error resetting terms signatures: ${error instanceof Error ? error.message : String(error)}`,
|
|
176
|
+
},
|
|
177
|
+
],
|
|
178
|
+
isError: true,
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
},
|
|
182
|
+
};
|
|
183
|
+
// ---------------------------------------------------------------------------
|
|
184
|
+
// update_terms_settings
|
|
185
|
+
// ---------------------------------------------------------------------------
|
|
186
|
+
export const updateTermsSettingsTool = {
|
|
187
|
+
name: 'update_terms_settings',
|
|
188
|
+
description: 'Update the Terms of Service enforcement settings: whether logged-in users must accept ' +
|
|
189
|
+
'the terms (tos_for_users) and whether the terms apply to public shares ' +
|
|
190
|
+
'(tos_on_public_shares). Requires the configured Nextcloud user to be an admin.',
|
|
191
|
+
inputSchema: z.object({
|
|
192
|
+
tosForUsers: z.boolean().optional().describe('Require logged-in users to accept the terms'),
|
|
193
|
+
tosOnPublicShares: z.boolean().optional().describe('Apply the terms to public shares'),
|
|
194
|
+
}),
|
|
195
|
+
handler: async (args) => {
|
|
196
|
+
const updates = [];
|
|
197
|
+
if (args.tosForUsers !== undefined)
|
|
198
|
+
updates.push(['tos_for_users', args.tosForUsers]);
|
|
199
|
+
if (args.tosOnPublicShares !== undefined) {
|
|
200
|
+
updates.push(['tos_on_public_shares', args.tosOnPublicShares]);
|
|
201
|
+
}
|
|
202
|
+
if (updates.length === 0) {
|
|
203
|
+
return {
|
|
204
|
+
content: [
|
|
205
|
+
{
|
|
206
|
+
type: 'text',
|
|
207
|
+
text: 'No settings provided. Specify tosForUsers and/or tosOnPublicShares.',
|
|
208
|
+
},
|
|
209
|
+
],
|
|
210
|
+
isError: true,
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
try {
|
|
214
|
+
const applied = [];
|
|
215
|
+
for (const [key, enabled] of updates) {
|
|
216
|
+
const value = enabled ? 'yes' : 'no';
|
|
217
|
+
await fetchOCS(`${APPCONFIG_BASE}/${key}`, {
|
|
218
|
+
method: 'POST',
|
|
219
|
+
body: { value },
|
|
220
|
+
});
|
|
221
|
+
applied.push(`${key} = ${value}`);
|
|
222
|
+
}
|
|
223
|
+
return {
|
|
224
|
+
content: [
|
|
225
|
+
{
|
|
226
|
+
type: 'text',
|
|
227
|
+
text: `Updated terms of service settings:\n${applied.map((u) => `- ${u}`).join('\n')}`,
|
|
228
|
+
},
|
|
229
|
+
],
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
catch (error) {
|
|
233
|
+
return {
|
|
234
|
+
content: [
|
|
235
|
+
{
|
|
236
|
+
type: 'text',
|
|
237
|
+
text: `Error updating terms settings: ${error instanceof Error ? error.message : String(error)}`,
|
|
238
|
+
},
|
|
239
|
+
],
|
|
240
|
+
isError: true,
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
},
|
|
244
|
+
};
|
|
245
|
+
// ---------------------------------------------------------------------------
|
|
246
|
+
// Export
|
|
247
|
+
// ---------------------------------------------------------------------------
|
|
248
|
+
export const termsOfServiceTools = [
|
|
249
|
+
getTermsOfServiceTool,
|
|
250
|
+
setTermsOfServiceTool,
|
|
251
|
+
deleteTermsOfServiceTool,
|
|
252
|
+
resetTermsSignaturesTool,
|
|
253
|
+
updateTermsSettingsTool,
|
|
254
|
+
];
|