@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.
Files changed (67) hide show
  1. package/dist/agents/auth-profiles/profiles.js +37 -0
  2. package/dist/agents/auth-profiles.js +1 -1
  3. package/dist/agents/pi-tools.policy.js +4 -1
  4. package/dist/agents/sandbox/constants.js +0 -1
  5. package/dist/agents/system-prompt.js +1 -4
  6. package/dist/agents/taskmaster-tools.js +14 -6
  7. package/dist/agents/tool-policy.js +5 -5
  8. package/dist/agents/tools/apikeys-tool.js +16 -5
  9. package/dist/agents/tools/contact-create-tool.js +59 -0
  10. package/dist/agents/tools/contact-delete-tool.js +48 -0
  11. package/dist/agents/tools/contact-update-tool.js +17 -2
  12. package/dist/agents/tools/file-delete-tool.js +137 -0
  13. package/dist/agents/tools/file-list-tool.js +127 -0
  14. package/dist/auto-reply/reply/commands-tts.js +7 -2
  15. package/dist/build-info.json +3 -3
  16. package/dist/cli/provision-seed.js +1 -3
  17. package/dist/commands/doctor-config-flow.js +13 -0
  18. package/dist/config/agent-tools-reconcile.js +53 -0
  19. package/dist/config/defaults.js +10 -1
  20. package/dist/config/legacy.migrations.part-3.js +26 -0
  21. package/dist/config/zod-schema.core.js +9 -1
  22. package/dist/config/zod-schema.js +1 -0
  23. package/dist/control-ui/assets/index-CPawOl_z.css +1 -0
  24. package/dist/control-ui/assets/{index-DwMopZij.js → index-DQ1kxYd4.js} +693 -598
  25. package/dist/control-ui/assets/index-DQ1kxYd4.js.map +1 -0
  26. package/dist/control-ui/index.html +2 -2
  27. package/dist/gateway/chat-sanitize.js +16 -2
  28. package/dist/gateway/config-reload.js +1 -0
  29. package/dist/gateway/media-http.js +32 -1
  30. package/dist/gateway/server-methods/apikeys.js +56 -4
  31. package/dist/gateway/server-methods/tts.js +11 -2
  32. package/dist/gateway/server.impl.js +15 -0
  33. package/dist/media-understanding/apply.js +35 -0
  34. package/dist/media-understanding/providers/deepgram/audio.js +1 -1
  35. package/dist/media-understanding/providers/google/audio.js +1 -1
  36. package/dist/media-understanding/providers/google/video.js +1 -1
  37. package/dist/media-understanding/providers/index.js +2 -0
  38. package/dist/media-understanding/providers/openai/audio.js +1 -1
  39. package/dist/media-understanding/providers/sherpa-onnx/index.js +10 -0
  40. package/dist/media-understanding/runner.js +61 -72
  41. package/dist/media-understanding/sherpa-onnx-local.js +223 -0
  42. package/dist/records/records-manager.js +10 -0
  43. package/dist/tts/tts.js +98 -10
  44. package/dist/web/auto-reply/monitor/process-message.js +1 -0
  45. package/dist/web/inbound/monitor.js +9 -1
  46. package/extensions/googlechat/node_modules/.bin/taskmaster +2 -2
  47. package/extensions/googlechat/package.json +2 -2
  48. package/extensions/line/node_modules/.bin/taskmaster +2 -2
  49. package/extensions/line/package.json +1 -1
  50. package/extensions/matrix/node_modules/.bin/taskmaster +2 -2
  51. package/extensions/matrix/package.json +1 -1
  52. package/extensions/msteams/node_modules/.bin/taskmaster +2 -2
  53. package/extensions/msteams/package.json +1 -1
  54. package/extensions/nostr/node_modules/.bin/taskmaster +2 -2
  55. package/extensions/nostr/package.json +1 -1
  56. package/extensions/zalo/node_modules/.bin/taskmaster +2 -2
  57. package/extensions/zalo/package.json +1 -1
  58. package/extensions/zalouser/node_modules/.bin/taskmaster +2 -2
  59. package/extensions/zalouser/package.json +1 -1
  60. package/package.json +3 -2
  61. package/scripts/postinstall.js +76 -0
  62. package/skills/business-assistant/references/crm.md +32 -8
  63. package/taskmaster-docs/USER-GUIDE.md +84 -5
  64. package/templates/beagle/agents/admin/AGENTS.md +4 -2
  65. package/templates/taskmaster/agents/admin/AGENTS.md +1 -0
  66. package/dist/control-ui/assets/index-DvB85yTz.css +0 -1
  67. 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",
@@ -18,7 +18,6 @@ export const DEFAULT_TOOL_ALLOW = [
18
18
  "image",
19
19
  "sessions_list",
20
20
  "sessions_history",
21
- "sessions_send",
22
21
  "sessions_spawn",
23
22
  "session_status",
24
23
  ];
@@ -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 use sessions_send(sessionKey, message)",
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
- "contact_lookup",
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, or 'remove' to delete a stored key. Valid providers: anthropic, google, tavily, openai, replicate, hume, brave, elevenlabs. Keys are applied immediately without restart.",
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 "remove".`);
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
+ }