@rubytech/taskmaster 1.2.0 → 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 -1
- package/dist/agents/sandbox/constants.js +0 -1
- package/dist/agents/system-prompt.js +1 -4
- package/dist/agents/taskmaster-tools.js +14 -6
- package/dist/agents/tool-policy.js +5 -5
- 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 -3
- 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-DwMopZij.js → index-DQ1kxYd4.js} +693 -598
- package/dist/control-ui/assets/index-DQ1kxYd4.js.map +1 -0
- package/dist/control-ui/index.html +2 -2
- package/dist/gateway/chat-sanitize.js +16 -2
- package/dist/gateway/config-reload.js +1 -0
- package/dist/gateway/media-http.js +32 -1
- 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-DvB85yTz.css +0 -1
- package/dist/control-ui/assets/index-DwMopZij.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
|
}
|
|
@@ -56,7 +60,6 @@ const DEFAULT_SUBAGENT_TOOL_DENY = [
|
|
|
56
60
|
// Session management - main agent orchestrates
|
|
57
61
|
"sessions_list",
|
|
58
62
|
"sessions_history",
|
|
59
|
-
"sessions_send",
|
|
60
63
|
"sessions_spawn",
|
|
61
64
|
// System admin - dangerous from subagent
|
|
62
65
|
"gateway",
|
|
@@ -81,7 +81,7 @@ function buildMessagingSection(params) {
|
|
|
81
81
|
return [
|
|
82
82
|
"## Messaging",
|
|
83
83
|
"- Reply in current session → automatically routes to the source channel (Signal, Telegram, etc.)",
|
|
84
|
-
"- Cross-session messaging
|
|
84
|
+
"- Cross-session messaging is not supported; reply within your current session.",
|
|
85
85
|
"- Never use exec/curl for provider messaging; Taskmaster handles all routing internally.",
|
|
86
86
|
params.availableTools.has("message")
|
|
87
87
|
? [
|
|
@@ -153,7 +153,6 @@ export function buildAgentSystemPrompt(params) {
|
|
|
153
153
|
agents_list: "List agent ids allowed for sessions_spawn",
|
|
154
154
|
sessions_list: "List other sessions (incl. sub-agents) with filters/last",
|
|
155
155
|
sessions_history: "Fetch history for another session/sub-agent",
|
|
156
|
-
sessions_send: "Send a message to another session/sub-agent",
|
|
157
156
|
sessions_spawn: "Spawn a sub-agent session",
|
|
158
157
|
session_status: "Show a /status-equivalent status card (usage + time + Reasoning/Verbose/Elevated); use for model-use questions (📊 session_status); optional per-session model override",
|
|
159
158
|
image: "Analyze an image with the configured image model",
|
|
@@ -182,7 +181,6 @@ export function buildAgentSystemPrompt(params) {
|
|
|
182
181
|
"agents_list",
|
|
183
182
|
"sessions_list",
|
|
184
183
|
"sessions_history",
|
|
185
|
-
"sessions_send",
|
|
186
184
|
"session_status",
|
|
187
185
|
"image",
|
|
188
186
|
];
|
|
@@ -299,7 +297,6 @@ export function buildAgentSystemPrompt(params) {
|
|
|
299
297
|
"- cron: manage scheduled events and wake events (use for reminders; when scheduling a reminder, write the systemEvent text as something that will read like a reminder when it fires, and mention that it is a reminder depending on the time gap between setting and firing; include recent context in reminder text if appropriate)",
|
|
300
298
|
"- sessions_list: list sessions",
|
|
301
299
|
"- sessions_history: fetch session history",
|
|
302
|
-
"- sessions_send: send to another session",
|
|
303
300
|
].join("\n"),
|
|
304
301
|
"TOOLS.md does not control tool availability; it is user guidance for how to use external tools.",
|
|
305
302
|
"If a task is more complex or takes longer, spawn a sub-agent. It will do the work for you and ping you when it's done. You can always check up on it.",
|
|
@@ -16,13 +16,16 @@ import { createNodesTool } from "./tools/nodes-tool.js";
|
|
|
16
16
|
import { createSessionStatusTool } from "./tools/session-status-tool.js";
|
|
17
17
|
import { createSessionsHistoryTool } from "./tools/sessions-history-tool.js";
|
|
18
18
|
import { createSessionsListTool } from "./tools/sessions-list-tool.js";
|
|
19
|
-
import { createSessionsSendTool } from "./tools/sessions-send-tool.js";
|
|
20
19
|
import { createSessionsSpawnTool } from "./tools/sessions-spawn-tool.js";
|
|
21
20
|
import { createWebFetchTool, createWebSearchTool } from "./tools/web-tools.js";
|
|
22
21
|
import { createTtsTool } from "./tools/tts-tool.js";
|
|
23
22
|
import { createCurrentTimeTool } from "./tools/current-time.js";
|
|
24
23
|
import { createAuthorizeAdminTool, createListAdminsTool, createRevokeAdminTool, } from "./tools/authorize-admin-tool.js";
|
|
25
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";
|
|
26
29
|
import { createContactLookupTool } from "./tools/contact-lookup-tool.js";
|
|
27
30
|
import { createContactUpdateTool } from "./tools/contact-update-tool.js";
|
|
28
31
|
import { createRelayMessageTool } from "./tools/relay-message-tool.js";
|
|
@@ -104,11 +107,6 @@ export function createTaskmasterTools(options) {
|
|
|
104
107
|
agentSessionKey: options?.agentSessionKey,
|
|
105
108
|
sandboxed: options?.sandboxed,
|
|
106
109
|
}),
|
|
107
|
-
createSessionsSendTool({
|
|
108
|
-
agentSessionKey: options?.agentSessionKey,
|
|
109
|
-
agentChannel: options?.agentChannel,
|
|
110
|
-
sandboxed: options?.sandboxed,
|
|
111
|
-
}),
|
|
112
110
|
createSessionsSpawnTool({
|
|
113
111
|
agentSessionKey: options?.agentSessionKey,
|
|
114
112
|
agentChannel: options?.agentChannel,
|
|
@@ -144,10 +142,20 @@ export function createTaskmasterTools(options) {
|
|
|
144
142
|
createRevokeAdminTool(),
|
|
145
143
|
createListAdminsTool(),
|
|
146
144
|
createLicenseGenerateTool(),
|
|
145
|
+
createContactCreateTool(),
|
|
146
|
+
createContactDeleteTool(),
|
|
147
147
|
createContactLookupTool(),
|
|
148
148
|
createContactUpdateTool(),
|
|
149
149
|
createRelayMessageTool(),
|
|
150
150
|
createApiKeysTool(),
|
|
151
|
+
createFileDeleteTool({
|
|
152
|
+
config: options?.config,
|
|
153
|
+
agentSessionKey: options?.agentSessionKey,
|
|
154
|
+
}),
|
|
155
|
+
createFileListTool({
|
|
156
|
+
config: options?.config,
|
|
157
|
+
agentSessionKey: options?.agentSessionKey,
|
|
158
|
+
}),
|
|
151
159
|
];
|
|
152
160
|
// Add scoped skill_read tool when skill directories are provided
|
|
153
161
|
const skillDirs = (options?.skillBaseDirs ?? []).filter(Boolean);
|
|
@@ -25,7 +25,6 @@ export const TOOL_GROUPS = {
|
|
|
25
25
|
"group:sessions": [
|
|
26
26
|
"sessions_list",
|
|
27
27
|
"sessions_history",
|
|
28
|
-
"sessions_send",
|
|
29
28
|
"sessions_spawn",
|
|
30
29
|
"session_status",
|
|
31
30
|
],
|
|
@@ -37,6 +36,8 @@ export const TOOL_GROUPS = {
|
|
|
37
36
|
"group:messaging": ["message"],
|
|
38
37
|
// Nodes + device tools
|
|
39
38
|
"group:nodes": ["nodes"],
|
|
39
|
+
// Contact record management
|
|
40
|
+
"group:contacts": ["contact_create", "contact_delete", "contact_lookup", "contact_update"],
|
|
40
41
|
// Admin management tools
|
|
41
42
|
"group:admin": ["authorize_admin", "revoke_admin", "list_admins"],
|
|
42
43
|
// All Taskmaster native tools (excludes provider plugins).
|
|
@@ -50,7 +51,6 @@ export const TOOL_GROUPS = {
|
|
|
50
51
|
"agents_list",
|
|
51
52
|
"sessions_list",
|
|
52
53
|
"sessions_history",
|
|
53
|
-
"sessions_send",
|
|
54
54
|
"sessions_spawn",
|
|
55
55
|
"session_status",
|
|
56
56
|
"memory_search",
|
|
@@ -69,6 +69,8 @@ export const TOOL_GROUPS = {
|
|
|
69
69
|
"revoke_admin",
|
|
70
70
|
"list_admins",
|
|
71
71
|
"api_keys",
|
|
72
|
+
"file_delete",
|
|
73
|
+
"file_list",
|
|
72
74
|
],
|
|
73
75
|
};
|
|
74
76
|
// Tools that are never granted by profiles — must be explicitly added to the
|
|
@@ -95,7 +97,6 @@ const TOOL_PROFILES = {
|
|
|
95
97
|
"group:messaging",
|
|
96
98
|
"sessions_list",
|
|
97
99
|
"sessions_history",
|
|
98
|
-
"sessions_send",
|
|
99
100
|
"session_status",
|
|
100
101
|
],
|
|
101
102
|
},
|
|
@@ -116,8 +117,7 @@ const TOOL_PROFILES = {
|
|
|
116
117
|
"image",
|
|
117
118
|
"tts",
|
|
118
119
|
"license_generate",
|
|
119
|
-
"
|
|
120
|
-
"contact_update",
|
|
120
|
+
"group:contacts",
|
|
121
121
|
"relay_message",
|
|
122
122
|
"memory_save_media",
|
|
123
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
|
+
}
|