@rubytech/taskmaster 1.2.1 → 1.3.0
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/agents/auth-profiles/profiles.js +37 -0
- package/dist/agents/auth-profiles.js +1 -1
- package/dist/agents/pi-tools.policy.js +4 -0
- package/dist/agents/taskmaster-tools.js +14 -0
- package/dist/agents/tool-policy.js +5 -2
- package/dist/agents/tools/apikeys-tool.js +16 -5
- package/dist/agents/tools/contact-create-tool.js +59 -0
- package/dist/agents/tools/contact-delete-tool.js +48 -0
- package/dist/agents/tools/contact-update-tool.js +17 -2
- package/dist/agents/tools/file-delete-tool.js +137 -0
- package/dist/agents/tools/file-list-tool.js +127 -0
- package/dist/auto-reply/reply/commands-tts.js +7 -2
- package/dist/build-info.json +3 -3
- package/dist/cli/provision-seed.js +1 -2
- package/dist/commands/doctor-config-flow.js +13 -0
- package/dist/config/agent-tools-reconcile.js +53 -0
- package/dist/config/defaults.js +10 -1
- package/dist/config/legacy.migrations.part-3.js +26 -0
- package/dist/config/zod-schema.core.js +9 -1
- package/dist/config/zod-schema.js +1 -0
- package/dist/control-ui/assets/index-CPawOl_z.css +1 -0
- package/dist/control-ui/assets/{index-N8du4fwV.js → index-DQ1kxYd4.js} +692 -598
- package/dist/control-ui/assets/index-DQ1kxYd4.js.map +1 -0
- package/dist/control-ui/index.html +2 -2
- package/dist/gateway/config-reload.js +1 -0
- package/dist/gateway/media-http.js +28 -0
- package/dist/gateway/server-methods/apikeys.js +56 -4
- package/dist/gateway/server-methods/tts.js +11 -2
- package/dist/gateway/server.impl.js +15 -0
- package/dist/media-understanding/apply.js +35 -0
- package/dist/media-understanding/providers/deepgram/audio.js +1 -1
- package/dist/media-understanding/providers/google/audio.js +1 -1
- package/dist/media-understanding/providers/google/video.js +1 -1
- package/dist/media-understanding/providers/index.js +2 -0
- package/dist/media-understanding/providers/openai/audio.js +1 -1
- package/dist/media-understanding/providers/sherpa-onnx/index.js +10 -0
- package/dist/media-understanding/runner.js +61 -72
- package/dist/media-understanding/sherpa-onnx-local.js +223 -0
- package/dist/records/records-manager.js +10 -0
- package/dist/tts/tts.js +98 -10
- package/dist/web/auto-reply/monitor/process-message.js +1 -0
- package/dist/web/inbound/monitor.js +9 -1
- package/extensions/googlechat/node_modules/.bin/taskmaster +2 -2
- package/extensions/googlechat/package.json +2 -2
- package/extensions/line/node_modules/.bin/taskmaster +2 -2
- package/extensions/line/package.json +1 -1
- package/extensions/matrix/node_modules/.bin/taskmaster +2 -2
- package/extensions/matrix/package.json +1 -1
- package/extensions/msteams/node_modules/.bin/taskmaster +2 -2
- package/extensions/msteams/package.json +1 -1
- package/extensions/nostr/node_modules/.bin/taskmaster +2 -2
- package/extensions/nostr/package.json +1 -1
- package/extensions/zalo/node_modules/.bin/taskmaster +2 -2
- package/extensions/zalo/package.json +1 -1
- package/extensions/zalouser/node_modules/.bin/taskmaster +2 -2
- package/extensions/zalouser/package.json +1 -1
- package/package.json +3 -2
- package/scripts/postinstall.js +76 -0
- package/skills/business-assistant/references/crm.md +32 -8
- package/taskmaster-docs/USER-GUIDE.md +84 -5
- package/templates/beagle/agents/admin/AGENTS.md +4 -2
- package/templates/taskmaster/agents/admin/AGENTS.md +1 -0
- package/dist/control-ui/assets/index-DtQHRIVD.css +0 -1
- package/dist/control-ui/assets/index-N8du4fwV.js.map +0 -1
|
@@ -33,6 +33,43 @@ export function upsertAuthProfile(params) {
|
|
|
33
33
|
store.profiles[params.profileId] = params.credential;
|
|
34
34
|
saveAuthProfileStore(store, params.agentDir);
|
|
35
35
|
}
|
|
36
|
+
/**
|
|
37
|
+
* Remove an auth profile and all associated state (lastGood, usageStats, order).
|
|
38
|
+
* Used when an API key is deleted via the UI so the stale credential is not
|
|
39
|
+
* picked up by provider resolution.
|
|
40
|
+
*/
|
|
41
|
+
export function removeAuthProfile(params) {
|
|
42
|
+
const store = ensureAuthProfileStore(params.agentDir);
|
|
43
|
+
if (!(params.profileId in store.profiles))
|
|
44
|
+
return;
|
|
45
|
+
const provider = store.profiles[params.profileId]?.provider;
|
|
46
|
+
delete store.profiles[params.profileId];
|
|
47
|
+
// Clean up lastGood if it pointed to this profile
|
|
48
|
+
if (provider && store.lastGood?.[provider] === params.profileId) {
|
|
49
|
+
delete store.lastGood[provider];
|
|
50
|
+
if (Object.keys(store.lastGood).length === 0) {
|
|
51
|
+
store.lastGood = undefined;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
// Clean up usageStats
|
|
55
|
+
if (store.usageStats?.[params.profileId]) {
|
|
56
|
+
delete store.usageStats[params.profileId];
|
|
57
|
+
if (Object.keys(store.usageStats).length === 0) {
|
|
58
|
+
store.usageStats = undefined;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
// Clean up order references
|
|
62
|
+
if (provider && store.order?.[provider]) {
|
|
63
|
+
store.order[provider] = store.order[provider].filter((id) => id !== params.profileId);
|
|
64
|
+
if (store.order[provider].length === 0) {
|
|
65
|
+
delete store.order[provider];
|
|
66
|
+
}
|
|
67
|
+
if (Object.keys(store.order).length === 0) {
|
|
68
|
+
store.order = undefined;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
saveAuthProfileStore(store, params.agentDir);
|
|
72
|
+
}
|
|
36
73
|
export function listProfilesForProvider(store, provider) {
|
|
37
74
|
const providerKey = normalizeProviderId(provider);
|
|
38
75
|
return Object.entries(store.profiles)
|
|
@@ -4,7 +4,7 @@ export { formatAuthDoctorHint } from "./auth-profiles/doctor.js";
|
|
|
4
4
|
export { resolveApiKeyForProfile } from "./auth-profiles/oauth.js";
|
|
5
5
|
export { resolveAuthProfileOrder } from "./auth-profiles/order.js";
|
|
6
6
|
export { resolveAuthStorePathForDisplay } from "./auth-profiles/paths.js";
|
|
7
|
-
export { listProfilesForProvider, markAuthProfileGood, setAuthProfileOrder, upsertAuthProfile, } from "./auth-profiles/profiles.js";
|
|
7
|
+
export { listProfilesForProvider, markAuthProfileGood, removeAuthProfile, setAuthProfileOrder, upsertAuthProfile, } from "./auth-profiles/profiles.js";
|
|
8
8
|
export { repairOAuthProfileIdMismatch, suggestOAuthProfileIdForLegacyDefault, } from "./auth-profiles/repair.js";
|
|
9
9
|
export { ensureAuthProfileStore, loadAuthProfileStore, saveAuthProfileStore, } from "./auth-profiles/store.js";
|
|
10
10
|
export { calculateAuthProfileCooldownMs, clearAuthProfileCooldown, isProfileInCooldown, markAuthProfileCooldown, markAuthProfileFailure, markAuthProfileUsed, resolveProfileUnusableUntilForDisplay, } from "./auth-profiles/usage.js";
|
|
@@ -49,6 +49,10 @@ function makeToolPolicyMatcher(policy) {
|
|
|
49
49
|
return true;
|
|
50
50
|
if (normalized === "apply_patch" && matchesAny("exec", allow))
|
|
51
51
|
return true;
|
|
52
|
+
if (normalized === "file_delete" && matchesAny("memory_write", allow))
|
|
53
|
+
return true;
|
|
54
|
+
if (normalized === "file_list" && matchesAny("memory_search", allow))
|
|
55
|
+
return true;
|
|
52
56
|
return false;
|
|
53
57
|
};
|
|
54
58
|
}
|
|
@@ -22,6 +22,10 @@ import { createTtsTool } from "./tools/tts-tool.js";
|
|
|
22
22
|
import { createCurrentTimeTool } from "./tools/current-time.js";
|
|
23
23
|
import { createAuthorizeAdminTool, createListAdminsTool, createRevokeAdminTool, } from "./tools/authorize-admin-tool.js";
|
|
24
24
|
import { createLicenseGenerateTool } from "./tools/license-tool.js";
|
|
25
|
+
import { createContactCreateTool } from "./tools/contact-create-tool.js";
|
|
26
|
+
import { createContactDeleteTool } from "./tools/contact-delete-tool.js";
|
|
27
|
+
import { createFileDeleteTool } from "./tools/file-delete-tool.js";
|
|
28
|
+
import { createFileListTool } from "./tools/file-list-tool.js";
|
|
25
29
|
import { createContactLookupTool } from "./tools/contact-lookup-tool.js";
|
|
26
30
|
import { createContactUpdateTool } from "./tools/contact-update-tool.js";
|
|
27
31
|
import { createRelayMessageTool } from "./tools/relay-message-tool.js";
|
|
@@ -138,10 +142,20 @@ export function createTaskmasterTools(options) {
|
|
|
138
142
|
createRevokeAdminTool(),
|
|
139
143
|
createListAdminsTool(),
|
|
140
144
|
createLicenseGenerateTool(),
|
|
145
|
+
createContactCreateTool(),
|
|
146
|
+
createContactDeleteTool(),
|
|
141
147
|
createContactLookupTool(),
|
|
142
148
|
createContactUpdateTool(),
|
|
143
149
|
createRelayMessageTool(),
|
|
144
150
|
createApiKeysTool(),
|
|
151
|
+
createFileDeleteTool({
|
|
152
|
+
config: options?.config,
|
|
153
|
+
agentSessionKey: options?.agentSessionKey,
|
|
154
|
+
}),
|
|
155
|
+
createFileListTool({
|
|
156
|
+
config: options?.config,
|
|
157
|
+
agentSessionKey: options?.agentSessionKey,
|
|
158
|
+
}),
|
|
145
159
|
];
|
|
146
160
|
// Add scoped skill_read tool when skill directories are provided
|
|
147
161
|
const skillDirs = (options?.skillBaseDirs ?? []).filter(Boolean);
|
|
@@ -36,6 +36,8 @@ export const TOOL_GROUPS = {
|
|
|
36
36
|
"group:messaging": ["message"],
|
|
37
37
|
// Nodes + device tools
|
|
38
38
|
"group:nodes": ["nodes"],
|
|
39
|
+
// Contact record management
|
|
40
|
+
"group:contacts": ["contact_create", "contact_delete", "contact_lookup", "contact_update"],
|
|
39
41
|
// Admin management tools
|
|
40
42
|
"group:admin": ["authorize_admin", "revoke_admin", "list_admins"],
|
|
41
43
|
// All Taskmaster native tools (excludes provider plugins).
|
|
@@ -67,6 +69,8 @@ export const TOOL_GROUPS = {
|
|
|
67
69
|
"revoke_admin",
|
|
68
70
|
"list_admins",
|
|
69
71
|
"api_keys",
|
|
72
|
+
"file_delete",
|
|
73
|
+
"file_list",
|
|
70
74
|
],
|
|
71
75
|
};
|
|
72
76
|
// Tools that are never granted by profiles — must be explicitly added to the
|
|
@@ -113,8 +117,7 @@ const TOOL_PROFILES = {
|
|
|
113
117
|
"image",
|
|
114
118
|
"tts",
|
|
115
119
|
"license_generate",
|
|
116
|
-
"
|
|
117
|
-
"contact_update",
|
|
120
|
+
"group:contacts",
|
|
118
121
|
"relay_message",
|
|
119
122
|
"memory_save_media",
|
|
120
123
|
"image_generate",
|
|
@@ -9,21 +9,24 @@ import { Type } from "@sinclair/typebox";
|
|
|
9
9
|
import { jsonResult, readStringParam } from "./common.js";
|
|
10
10
|
import { callGatewayTool } from "./gateway.js";
|
|
11
11
|
const ApiKeysSchema = Type.Object({
|
|
12
|
-
action: Type.Union([Type.Literal("set"), Type.Literal("list"), Type.Literal("remove")], {
|
|
13
|
-
description: 'Action to perform: "set" stores a key, "list" shows which providers have keys configured, "remove" deletes a stored key.',
|
|
12
|
+
action: Type.Union([Type.Literal("set"), Type.Literal("list"), Type.Literal("remove"), Type.Literal("disable")], {
|
|
13
|
+
description: 'Action to perform: "set" stores a key, "list" shows which providers have keys configured, "remove" deletes a stored key, "disable" toggles a key on/off without deleting it.',
|
|
14
14
|
}),
|
|
15
15
|
provider: Type.Optional(Type.String({
|
|
16
|
-
description: "Provider ID (required for set/remove). Valid providers: anthropic, google, tavily, openai, replicate, hume, brave, elevenlabs.",
|
|
16
|
+
description: "Provider ID (required for set/remove/disable). Valid providers: anthropic, google, tavily, openai, replicate, hume, brave, elevenlabs.",
|
|
17
17
|
})),
|
|
18
18
|
apiKey: Type.Optional(Type.String({
|
|
19
19
|
description: "The API key value (required for set).",
|
|
20
20
|
})),
|
|
21
|
+
disabled: Type.Optional(Type.Boolean({
|
|
22
|
+
description: "Whether to disable (true) or enable (false) the key (required for disable).",
|
|
23
|
+
})),
|
|
21
24
|
});
|
|
22
25
|
export function createApiKeysTool() {
|
|
23
26
|
return {
|
|
24
27
|
label: "API Keys",
|
|
25
28
|
name: "api_keys",
|
|
26
|
-
description: "Manage API keys for external providers. Use action 'set' to store a key (provider + apiKey required), 'list' to check which providers have keys configured,
|
|
29
|
+
description: "Manage API keys for external providers. Use action 'set' to store a key (provider + apiKey required), 'list' to check which providers have keys configured, 'remove' to delete a stored key, or 'disable' to toggle a key on/off without deleting it (provider + disabled required). Valid providers: anthropic, google, tavily, openai, replicate, hume, brave, elevenlabs. Keys are applied immediately without restart.",
|
|
27
30
|
parameters: ApiKeysSchema,
|
|
28
31
|
execute: async (_toolCallId, args) => {
|
|
29
32
|
const params = args;
|
|
@@ -44,8 +47,16 @@ export function createApiKeysTool() {
|
|
|
44
47
|
const result = await callGatewayTool("apikeys.remove", {}, { provider });
|
|
45
48
|
return jsonResult(result);
|
|
46
49
|
}
|
|
50
|
+
case "disable": {
|
|
51
|
+
const provider = readStringParam(params, "provider", { required: true });
|
|
52
|
+
const disabled = params.disabled;
|
|
53
|
+
if (typeof disabled !== "boolean")
|
|
54
|
+
throw new Error("disabled parameter must be a boolean");
|
|
55
|
+
const result = await callGatewayTool("apikeys.disable", {}, { provider, disabled });
|
|
56
|
+
return jsonResult(result);
|
|
57
|
+
}
|
|
47
58
|
default:
|
|
48
|
-
throw new Error(`Unknown action: ${action}. Use "set", "list", or "
|
|
59
|
+
throw new Error(`Unknown action: ${action}. Use "set", "list", "remove", or "disable".`);
|
|
49
60
|
}
|
|
50
61
|
},
|
|
51
62
|
};
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { Type } from "@sinclair/typebox";
|
|
2
|
+
import { getRecord, setRecord } from "../../records/records-manager.js";
|
|
3
|
+
import { jsonResult } from "./common.js";
|
|
4
|
+
const ContactCreateSchema = Type.Object({
|
|
5
|
+
phone: Type.String({
|
|
6
|
+
description: "Phone number (record ID) in international format (e.g. '+447490553305').",
|
|
7
|
+
}),
|
|
8
|
+
name: Type.String({
|
|
9
|
+
description: "Full name of the contact (e.g. 'Mrs Sarah Jenkins').",
|
|
10
|
+
}),
|
|
11
|
+
fields: Type.Optional(Type.Record(Type.String(), Type.String(), {
|
|
12
|
+
description: "Optional initial fields to set on the contact (e.g. { status: 'enquiry', source: 'WhatsApp' }).",
|
|
13
|
+
})),
|
|
14
|
+
});
|
|
15
|
+
/**
|
|
16
|
+
* Contact record creation tool — admin agent only.
|
|
17
|
+
*
|
|
18
|
+
* Creates a new contact record in the secure records store.
|
|
19
|
+
* The contact record is the source of truth — its phone number and name
|
|
20
|
+
* are the canonical identifiers that link to user-scoped memory at
|
|
21
|
+
* `memory/users/{phone}/`.
|
|
22
|
+
*/
|
|
23
|
+
export function createContactCreateTool() {
|
|
24
|
+
return {
|
|
25
|
+
label: "Contact Create",
|
|
26
|
+
name: "contact_create",
|
|
27
|
+
description: "Create a new contact record. The contact record is the source of truth — " +
|
|
28
|
+
"its phone number and name are the canonical identifiers linking to user-scoped memory. " +
|
|
29
|
+
"Use this on first meaningful interaction when you have a name and phone number. " +
|
|
30
|
+
"Fails if a record already exists for this phone number.",
|
|
31
|
+
parameters: ContactCreateSchema,
|
|
32
|
+
execute: async (_toolCallId, args) => {
|
|
33
|
+
const params = args;
|
|
34
|
+
const phone = typeof params.phone === "string" ? params.phone.trim() : "";
|
|
35
|
+
const name = typeof params.name === "string" ? params.name.trim() : "";
|
|
36
|
+
const fields = params.fields && typeof params.fields === "object" && !Array.isArray(params.fields)
|
|
37
|
+
? params.fields
|
|
38
|
+
: {};
|
|
39
|
+
if (!phone) {
|
|
40
|
+
return jsonResult({ error: "Phone number is required." });
|
|
41
|
+
}
|
|
42
|
+
if (!name) {
|
|
43
|
+
return jsonResult({ error: "Name is required." });
|
|
44
|
+
}
|
|
45
|
+
const existing = getRecord(phone);
|
|
46
|
+
if (existing) {
|
|
47
|
+
return jsonResult({
|
|
48
|
+
error: `A contact record already exists for ${phone} (${existing.name}). Use contact_update to modify it.`,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
const record = setRecord(phone, { name, fields });
|
|
52
|
+
return jsonResult({
|
|
53
|
+
ok: true,
|
|
54
|
+
action: "created",
|
|
55
|
+
record,
|
|
56
|
+
});
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { Type } from "@sinclair/typebox";
|
|
2
|
+
import { getRecord, deleteRecord } from "../../records/records-manager.js";
|
|
3
|
+
import { jsonResult } from "./common.js";
|
|
4
|
+
const ContactDeleteSchema = Type.Object({
|
|
5
|
+
phone: Type.String({
|
|
6
|
+
description: "Phone number (record ID) of the contact to delete (e.g. '+447490553305').",
|
|
7
|
+
}),
|
|
8
|
+
});
|
|
9
|
+
/**
|
|
10
|
+
* Contact record deletion tool — admin agent only.
|
|
11
|
+
*
|
|
12
|
+
* Deletes a contact record from the secure records store.
|
|
13
|
+
* Does NOT delete associated user-scoped memory at `memory/users/{phone}/` —
|
|
14
|
+
* that is a separate decision for the business owner.
|
|
15
|
+
*/
|
|
16
|
+
export function createContactDeleteTool() {
|
|
17
|
+
return {
|
|
18
|
+
label: "Contact Delete",
|
|
19
|
+
name: "contact_delete",
|
|
20
|
+
description: "Delete a contact record. This removes the structured record only — " +
|
|
21
|
+
"user-scoped memory at memory/users/{phone}/ is not affected. " +
|
|
22
|
+
"Use when a contact is no longer relevant (spam, duplicate, etc.).",
|
|
23
|
+
parameters: ContactDeleteSchema,
|
|
24
|
+
execute: async (_toolCallId, args) => {
|
|
25
|
+
const params = args;
|
|
26
|
+
const phone = typeof params.phone === "string" ? params.phone.trim() : "";
|
|
27
|
+
if (!phone) {
|
|
28
|
+
return jsonResult({ error: "Phone number is required." });
|
|
29
|
+
}
|
|
30
|
+
const existing = getRecord(phone);
|
|
31
|
+
if (!existing) {
|
|
32
|
+
return jsonResult({
|
|
33
|
+
error: `No contact record found for ${phone}.`,
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
const deleted = deleteRecord(phone);
|
|
37
|
+
if (!deleted) {
|
|
38
|
+
return jsonResult({ error: `Failed to delete contact record for ${phone}.` });
|
|
39
|
+
}
|
|
40
|
+
return jsonResult({
|
|
41
|
+
ok: true,
|
|
42
|
+
action: "deleted",
|
|
43
|
+
phone,
|
|
44
|
+
name: existing.name,
|
|
45
|
+
});
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
}
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { Type } from "@sinclair/typebox";
|
|
2
|
-
import { getRecord, setRecordField, deleteRecordField } from "../../records/records-manager.js";
|
|
2
|
+
import { getRecord, setRecordField, deleteRecordField, setRecordName, } from "../../records/records-manager.js";
|
|
3
3
|
import { jsonResult } from "./common.js";
|
|
4
4
|
const ContactUpdateSchema = Type.Object({
|
|
5
5
|
phone: Type.String({
|
|
6
6
|
description: "Phone number (record ID) of the contact to update (e.g. '+447490553305').",
|
|
7
7
|
}),
|
|
8
8
|
field: Type.String({
|
|
9
|
-
description: "Field name to set or delete (e.g. 'license_key', 'notes')."
|
|
9
|
+
description: "Field name to set or delete (e.g. 'license_key', 'notes'). " +
|
|
10
|
+
"Use 'name' to update the contact's display name.",
|
|
10
11
|
}),
|
|
11
12
|
value: Type.Optional(Type.String({
|
|
12
13
|
description: "Value to set. Omit to delete the field.",
|
|
@@ -44,6 +45,20 @@ export function createContactUpdateTool() {
|
|
|
44
45
|
error: `No contact record found for ${phone}. Create one via the Contacts admin page first.`,
|
|
45
46
|
});
|
|
46
47
|
}
|
|
48
|
+
// "name" updates the contact's canonical display name, not a custom field.
|
|
49
|
+
if (field === "name") {
|
|
50
|
+
if (value === undefined) {
|
|
51
|
+
return jsonResult({ error: "Cannot delete the contact name. Provide a new value instead." });
|
|
52
|
+
}
|
|
53
|
+
const updated = setRecordName(phone, value);
|
|
54
|
+
return jsonResult({
|
|
55
|
+
ok: true,
|
|
56
|
+
action: "renamed",
|
|
57
|
+
phone,
|
|
58
|
+
name: value,
|
|
59
|
+
record: updated,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
47
62
|
if (value === undefined) {
|
|
48
63
|
const updated = deleteRecordField(phone, field);
|
|
49
64
|
return jsonResult({
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { realpath } from "node:fs/promises";
|
|
4
|
+
import { Type } from "@sinclair/typebox";
|
|
5
|
+
import { resolveAgentWorkspaceRoot, resolveSessionAgentId } from "../agent-scope.js";
|
|
6
|
+
import { jsonResult, readStringParam } from "./common.js";
|
|
7
|
+
const FileDeleteSchema = Type.Object({
|
|
8
|
+
path: Type.String({
|
|
9
|
+
description: "Path to delete, relative to the workspace root " +
|
|
10
|
+
"(e.g. 'uploads/old-photo.jpg', 'memory/users/+447734875155/scratch.md').",
|
|
11
|
+
}),
|
|
12
|
+
recursive: Type.Optional(Type.Boolean({
|
|
13
|
+
description: "Set to true to delete a non-empty directory and all its contents. " +
|
|
14
|
+
"Defaults to false — non-empty directories are refused without this flag.",
|
|
15
|
+
})),
|
|
16
|
+
});
|
|
17
|
+
/**
|
|
18
|
+
* Protected top-level entries that cannot be deleted.
|
|
19
|
+
* These are structural directories/files whose removal would break the workspace.
|
|
20
|
+
*/
|
|
21
|
+
const PROTECTED_TOP_LEVEL = new Set([
|
|
22
|
+
"agents",
|
|
23
|
+
"IDENTITY.md",
|
|
24
|
+
"SOUL.md",
|
|
25
|
+
"AGENTS.md",
|
|
26
|
+
"TOOLS.md",
|
|
27
|
+
]);
|
|
28
|
+
/**
|
|
29
|
+
* Validate that `target` is strictly inside `root` (not equal to root).
|
|
30
|
+
* Both paths must be real (symlinks resolved) before calling.
|
|
31
|
+
*/
|
|
32
|
+
function isInsideRoot(target, root) {
|
|
33
|
+
const prefix = root.endsWith("/") ? root : `${root}/`;
|
|
34
|
+
return target.startsWith(prefix);
|
|
35
|
+
}
|
|
36
|
+
export function createFileDeleteTool(options) {
|
|
37
|
+
return {
|
|
38
|
+
label: "File Delete",
|
|
39
|
+
name: "file_delete",
|
|
40
|
+
description: "Delete a file or folder in the workspace. " +
|
|
41
|
+
"Path is relative to the workspace root. " +
|
|
42
|
+
"For non-empty directories, set recursive=true. " +
|
|
43
|
+
"Cannot delete protected structural paths (agents/, IDENTITY.md, SOUL.md, etc.).",
|
|
44
|
+
parameters: FileDeleteSchema,
|
|
45
|
+
execute: async (_toolCallId, args) => {
|
|
46
|
+
const params = args;
|
|
47
|
+
const rawPath = readStringParam(params, "path", { required: true });
|
|
48
|
+
const recursive = typeof params.recursive === "boolean" ? params.recursive : false;
|
|
49
|
+
// ── Resolve workspace root ──────────────────────────────────
|
|
50
|
+
const cfg = options?.config;
|
|
51
|
+
if (!cfg) {
|
|
52
|
+
return jsonResult({ error: "No config available — cannot resolve workspace." });
|
|
53
|
+
}
|
|
54
|
+
const agentId = resolveSessionAgentId({
|
|
55
|
+
sessionKey: options?.agentSessionKey,
|
|
56
|
+
config: cfg,
|
|
57
|
+
});
|
|
58
|
+
const workspaceRoot = resolveAgentWorkspaceRoot(cfg, agentId);
|
|
59
|
+
// ── Normalise and resolve target ────────────────────────────
|
|
60
|
+
// Reject obviously malicious patterns before touching the filesystem.
|
|
61
|
+
if (rawPath.includes("\0")) {
|
|
62
|
+
return jsonResult({ error: "Invalid path." });
|
|
63
|
+
}
|
|
64
|
+
const resolved = path.resolve(workspaceRoot, rawPath);
|
|
65
|
+
// ── Containment check (pre-realpath) ────────────────────────
|
|
66
|
+
if (!isInsideRoot(resolved, workspaceRoot)) {
|
|
67
|
+
return jsonResult({ error: "Path is outside the workspace." });
|
|
68
|
+
}
|
|
69
|
+
// ── Check existence ─────────────────────────────────────────
|
|
70
|
+
let stat;
|
|
71
|
+
try {
|
|
72
|
+
stat = await fs.lstat(resolved);
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
return jsonResult({ error: `Not found: ${rawPath}` });
|
|
76
|
+
}
|
|
77
|
+
// ── Containment check (post-realpath, catches symlink escapes) ─
|
|
78
|
+
let realTarget;
|
|
79
|
+
try {
|
|
80
|
+
realTarget = await realpath(resolved);
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
// If realpath fails but lstat succeeded, it's a broken symlink — safe to delete.
|
|
84
|
+
realTarget = resolved;
|
|
85
|
+
}
|
|
86
|
+
let realRoot;
|
|
87
|
+
try {
|
|
88
|
+
realRoot = await realpath(workspaceRoot);
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
return jsonResult({ error: "Workspace root is not accessible." });
|
|
92
|
+
}
|
|
93
|
+
if (!isInsideRoot(realTarget, realRoot)) {
|
|
94
|
+
return jsonResult({ error: "Path resolves outside the workspace." });
|
|
95
|
+
}
|
|
96
|
+
// ── Protected path check ────────────────────────────────────
|
|
97
|
+
const relative = path.relative(realRoot, realTarget);
|
|
98
|
+
const topSegment = relative.split(path.sep)[0];
|
|
99
|
+
if (topSegment && PROTECTED_TOP_LEVEL.has(topSegment)) {
|
|
100
|
+
return jsonResult({
|
|
101
|
+
error: `Cannot delete protected path: ${topSegment} is a structural workspace entry.`,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
// ── Delete ──────────────────────────────────────────────────
|
|
105
|
+
try {
|
|
106
|
+
if (stat.isDirectory()) {
|
|
107
|
+
if (recursive) {
|
|
108
|
+
await fs.rm(resolved, { recursive: true, force: true });
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
// rmdir only works on empty directories
|
|
112
|
+
await fs.rmdir(resolved);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
await fs.unlink(resolved);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
catch (err) {
|
|
120
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
121
|
+
if (message.includes("not empty") || message.includes("ENOTEMPTY")) {
|
|
122
|
+
return jsonResult({
|
|
123
|
+
error: `Directory is not empty. Set recursive=true to delete '${rawPath}' and all its contents.`,
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
return jsonResult({ error: `Failed to delete: ${message}` });
|
|
127
|
+
}
|
|
128
|
+
return jsonResult({
|
|
129
|
+
ok: true,
|
|
130
|
+
action: "deleted",
|
|
131
|
+
path: rawPath,
|
|
132
|
+
type: stat.isDirectory() ? "directory" : "file",
|
|
133
|
+
recursive,
|
|
134
|
+
});
|
|
135
|
+
},
|
|
136
|
+
};
|
|
137
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { realpath } from "node:fs/promises";
|
|
4
|
+
import { Type } from "@sinclair/typebox";
|
|
5
|
+
import { resolveAgentWorkspaceRoot, resolveSessionAgentId } from "../agent-scope.js";
|
|
6
|
+
import { jsonResult, readStringParam, readNumberParam } from "./common.js";
|
|
7
|
+
const FileListSchema = Type.Object({
|
|
8
|
+
path: Type.Optional(Type.String({
|
|
9
|
+
description: "Directory to list, relative to the workspace root " +
|
|
10
|
+
"(e.g. 'memory/users', 'skills'). Defaults to the workspace root.",
|
|
11
|
+
})),
|
|
12
|
+
depth: Type.Optional(Type.Number({
|
|
13
|
+
description: "How many levels deep to recurse. 1 = immediate children only (default). " +
|
|
14
|
+
"Use 2–3 for a broader view. Maximum 5.",
|
|
15
|
+
})),
|
|
16
|
+
});
|
|
17
|
+
function isInsideRoot(target, root) {
|
|
18
|
+
const prefix = root.endsWith("/") ? root : `${root}/`;
|
|
19
|
+
return target === root || target.startsWith(prefix);
|
|
20
|
+
}
|
|
21
|
+
async function listDir(dirPath, currentDepth, maxDepth) {
|
|
22
|
+
let entries;
|
|
23
|
+
try {
|
|
24
|
+
entries = await fs.readdir(dirPath, { withFileTypes: true });
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return [];
|
|
28
|
+
}
|
|
29
|
+
const result = [];
|
|
30
|
+
// Sort: directories first, then files, alphabetical within each group
|
|
31
|
+
const sorted = [...entries].sort((a, b) => {
|
|
32
|
+
const aDir = a.isDirectory() || a.isSymbolicLink() ? 0 : 1;
|
|
33
|
+
const bDir = b.isDirectory() || b.isSymbolicLink() ? 0 : 1;
|
|
34
|
+
if (aDir !== bDir)
|
|
35
|
+
return aDir - bDir;
|
|
36
|
+
return String(a.name).localeCompare(String(b.name));
|
|
37
|
+
});
|
|
38
|
+
for (const entry of sorted) {
|
|
39
|
+
const name = String(entry.name);
|
|
40
|
+
const fullPath = path.join(dirPath, name);
|
|
41
|
+
const item = {
|
|
42
|
+
name,
|
|
43
|
+
type: entry.isDirectory() ? "directory" : entry.isSymbolicLink() ? "symlink" : "file",
|
|
44
|
+
};
|
|
45
|
+
if (entry.isFile()) {
|
|
46
|
+
try {
|
|
47
|
+
const stat = await fs.stat(fullPath);
|
|
48
|
+
item.size = stat.size;
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
// stat failed — skip size
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
if ((entry.isDirectory() || entry.isSymbolicLink()) && currentDepth < maxDepth) {
|
|
55
|
+
item.children = await listDir(fullPath, currentDepth + 1, maxDepth);
|
|
56
|
+
}
|
|
57
|
+
result.push(item);
|
|
58
|
+
}
|
|
59
|
+
return result;
|
|
60
|
+
}
|
|
61
|
+
export function createFileListTool(options) {
|
|
62
|
+
return {
|
|
63
|
+
label: "File List",
|
|
64
|
+
name: "file_list",
|
|
65
|
+
description: "List files and folders in the workspace. " +
|
|
66
|
+
"Path is relative to the workspace root. " +
|
|
67
|
+
"Returns names, types (file/directory/symlink), and sizes. " +
|
|
68
|
+
"Use depth > 1 to see nested contents.",
|
|
69
|
+
parameters: FileListSchema,
|
|
70
|
+
execute: async (_toolCallId, args) => {
|
|
71
|
+
const params = args;
|
|
72
|
+
const rawPath = readStringParam(params, "path") ?? "";
|
|
73
|
+
const rawDepth = readNumberParam(params, "depth", { integer: true });
|
|
74
|
+
const maxDepth = Math.max(1, Math.min(rawDepth ?? 1, 5));
|
|
75
|
+
// ── Resolve workspace root ──────────────────────────────────
|
|
76
|
+
const cfg = options?.config;
|
|
77
|
+
if (!cfg) {
|
|
78
|
+
return jsonResult({ error: "No config available — cannot resolve workspace." });
|
|
79
|
+
}
|
|
80
|
+
const agentId = resolveSessionAgentId({
|
|
81
|
+
sessionKey: options?.agentSessionKey,
|
|
82
|
+
config: cfg,
|
|
83
|
+
});
|
|
84
|
+
const workspaceRoot = resolveAgentWorkspaceRoot(cfg, agentId);
|
|
85
|
+
// ── Normalise and resolve target ────────────────────────────
|
|
86
|
+
if (rawPath.includes("\0")) {
|
|
87
|
+
return jsonResult({ error: "Invalid path." });
|
|
88
|
+
}
|
|
89
|
+
const resolved = rawPath ? path.resolve(workspaceRoot, rawPath) : workspaceRoot;
|
|
90
|
+
// ── Containment check ───────────────────────────────────────
|
|
91
|
+
let realTarget;
|
|
92
|
+
try {
|
|
93
|
+
realTarget = await realpath(resolved);
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
return jsonResult({ error: `Not found: ${rawPath || "/"}` });
|
|
97
|
+
}
|
|
98
|
+
let realRoot;
|
|
99
|
+
try {
|
|
100
|
+
realRoot = await realpath(workspaceRoot);
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
return jsonResult({ error: "Workspace root is not accessible." });
|
|
104
|
+
}
|
|
105
|
+
if (!isInsideRoot(realTarget, realRoot)) {
|
|
106
|
+
return jsonResult({ error: "Path is outside the workspace." });
|
|
107
|
+
}
|
|
108
|
+
// ── Verify it's a directory ─────────────────────────────────
|
|
109
|
+
try {
|
|
110
|
+
const stat = await fs.stat(realTarget);
|
|
111
|
+
if (!stat.isDirectory()) {
|
|
112
|
+
return jsonResult({ error: `Not a directory: ${rawPath}` });
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
return jsonResult({ error: `Not found: ${rawPath || "/"}` });
|
|
117
|
+
}
|
|
118
|
+
// ── List ────────────────────────────────────────────────────
|
|
119
|
+
const entries = await listDir(realTarget, 1, maxDepth);
|
|
120
|
+
return jsonResult({
|
|
121
|
+
path: rawPath || "/",
|
|
122
|
+
depth: maxDepth,
|
|
123
|
+
entries,
|
|
124
|
+
});
|
|
125
|
+
},
|
|
126
|
+
};
|
|
127
|
+
}
|
|
@@ -115,6 +115,7 @@ export const handleTtsCommands = async (params, allowTextCommands) => {
|
|
|
115
115
|
.filter((provider) => isTtsProviderConfigured(config, provider));
|
|
116
116
|
const hasOpenAI = Boolean(resolveTtsApiKey(config, "openai"));
|
|
117
117
|
const hasElevenLabs = Boolean(resolveTtsApiKey(config, "elevenlabs"));
|
|
118
|
+
const hasHume = Boolean(resolveTtsApiKey(config, "hume"));
|
|
118
119
|
const hasEdge = isTtsProviderConfigured(config, "edge");
|
|
119
120
|
return {
|
|
120
121
|
shouldContinue: false,
|
|
@@ -124,13 +125,17 @@ export const handleTtsCommands = async (params, allowTextCommands) => {
|
|
|
124
125
|
`Fallbacks: ${fallback.join(", ") || "none"}\n` +
|
|
125
126
|
`OpenAI key: ${hasOpenAI ? "✅" : "❌"}\n` +
|
|
126
127
|
`ElevenLabs key: ${hasElevenLabs ? "✅" : "❌"}\n` +
|
|
128
|
+
`Hume key: ${hasHume ? "✅" : "❌"}\n` +
|
|
127
129
|
`Edge enabled: ${hasEdge ? "✅" : "❌"}\n` +
|
|
128
|
-
`Usage: /tts provider openai | elevenlabs | edge`,
|
|
130
|
+
`Usage: /tts provider openai | elevenlabs | hume | edge`,
|
|
129
131
|
},
|
|
130
132
|
};
|
|
131
133
|
}
|
|
132
134
|
const requested = args.trim().toLowerCase();
|
|
133
|
-
if (requested !== "openai" &&
|
|
135
|
+
if (requested !== "openai" &&
|
|
136
|
+
requested !== "elevenlabs" &&
|
|
137
|
+
requested !== "hume" &&
|
|
138
|
+
requested !== "edge") {
|
|
134
139
|
return { shouldContinue: false, reply: ttsUsage() };
|
|
135
140
|
}
|
|
136
141
|
setTtsProvider(prefsPath, requested);
|
package/dist/build-info.json
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { TaskmasterSchema, CONFIG_PATH_TASKMASTER, migrateLegacyConfig, readConfigFileSnapshot, } from "../config/config.js";
|
|
2
|
+
import { reconcileAgentContactTools } from "../config/agent-tools-reconcile.js";
|
|
2
3
|
import { applyPluginAutoEnable } from "../config/plugin-auto-enable.js";
|
|
3
4
|
import { formatCliCommand } from "../cli/command-format.js";
|
|
4
5
|
import { note } from "../terminal/note.js";
|
|
@@ -156,6 +157,18 @@ export async function loadAndMaybeMigrateDoctorConfig(params) {
|
|
|
156
157
|
fixHints.push(`Run "${formatCliCommand("taskmaster doctor --fix")}" to apply these changes.`);
|
|
157
158
|
}
|
|
158
159
|
}
|
|
160
|
+
const toolReconcile = reconcileAgentContactTools({ config: candidate });
|
|
161
|
+
if (toolReconcile.changes.length > 0) {
|
|
162
|
+
note(toolReconcile.changes.join("\n"), "Doctor changes");
|
|
163
|
+
candidate = toolReconcile.config;
|
|
164
|
+
pendingChanges = true;
|
|
165
|
+
if (shouldRepair) {
|
|
166
|
+
cfg = toolReconcile.config;
|
|
167
|
+
}
|
|
168
|
+
else {
|
|
169
|
+
fixHints.push(`Run "${formatCliCommand("taskmaster doctor --fix")}" to apply these changes.`);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
159
172
|
const unknown = stripUnknownConfigKeys(candidate);
|
|
160
173
|
if (unknown.removed.length > 0) {
|
|
161
174
|
const lines = unknown.removed.map((path) => `- ${path}`).join("\n");
|