@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.
Files changed (64) 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 -0
  4. package/dist/agents/taskmaster-tools.js +14 -0
  5. package/dist/agents/tool-policy.js +5 -2
  6. package/dist/agents/tools/apikeys-tool.js +16 -5
  7. package/dist/agents/tools/contact-create-tool.js +59 -0
  8. package/dist/agents/tools/contact-delete-tool.js +48 -0
  9. package/dist/agents/tools/contact-update-tool.js +17 -2
  10. package/dist/agents/tools/file-delete-tool.js +137 -0
  11. package/dist/agents/tools/file-list-tool.js +127 -0
  12. package/dist/auto-reply/reply/commands-tts.js +7 -2
  13. package/dist/build-info.json +3 -3
  14. package/dist/cli/provision-seed.js +1 -2
  15. package/dist/commands/doctor-config-flow.js +13 -0
  16. package/dist/config/agent-tools-reconcile.js +53 -0
  17. package/dist/config/defaults.js +10 -1
  18. package/dist/config/legacy.migrations.part-3.js +26 -0
  19. package/dist/config/zod-schema.core.js +9 -1
  20. package/dist/config/zod-schema.js +1 -0
  21. package/dist/control-ui/assets/index-CPawOl_z.css +1 -0
  22. package/dist/control-ui/assets/{index-N8du4fwV.js → index-DQ1kxYd4.js} +692 -598
  23. package/dist/control-ui/assets/index-DQ1kxYd4.js.map +1 -0
  24. package/dist/control-ui/index.html +2 -2
  25. package/dist/gateway/config-reload.js +1 -0
  26. package/dist/gateway/media-http.js +28 -0
  27. package/dist/gateway/server-methods/apikeys.js +56 -4
  28. package/dist/gateway/server-methods/tts.js +11 -2
  29. package/dist/gateway/server.impl.js +15 -0
  30. package/dist/media-understanding/apply.js +35 -0
  31. package/dist/media-understanding/providers/deepgram/audio.js +1 -1
  32. package/dist/media-understanding/providers/google/audio.js +1 -1
  33. package/dist/media-understanding/providers/google/video.js +1 -1
  34. package/dist/media-understanding/providers/index.js +2 -0
  35. package/dist/media-understanding/providers/openai/audio.js +1 -1
  36. package/dist/media-understanding/providers/sherpa-onnx/index.js +10 -0
  37. package/dist/media-understanding/runner.js +61 -72
  38. package/dist/media-understanding/sherpa-onnx-local.js +223 -0
  39. package/dist/records/records-manager.js +10 -0
  40. package/dist/tts/tts.js +98 -10
  41. package/dist/web/auto-reply/monitor/process-message.js +1 -0
  42. package/dist/web/inbound/monitor.js +9 -1
  43. package/extensions/googlechat/node_modules/.bin/taskmaster +2 -2
  44. package/extensions/googlechat/package.json +2 -2
  45. package/extensions/line/node_modules/.bin/taskmaster +2 -2
  46. package/extensions/line/package.json +1 -1
  47. package/extensions/matrix/node_modules/.bin/taskmaster +2 -2
  48. package/extensions/matrix/package.json +1 -1
  49. package/extensions/msteams/node_modules/.bin/taskmaster +2 -2
  50. package/extensions/msteams/package.json +1 -1
  51. package/extensions/nostr/node_modules/.bin/taskmaster +2 -2
  52. package/extensions/nostr/package.json +1 -1
  53. package/extensions/zalo/node_modules/.bin/taskmaster +2 -2
  54. package/extensions/zalo/package.json +1 -1
  55. package/extensions/zalouser/node_modules/.bin/taskmaster +2 -2
  56. package/extensions/zalouser/package.json +1 -1
  57. package/package.json +3 -2
  58. package/scripts/postinstall.js +76 -0
  59. package/skills/business-assistant/references/crm.md +32 -8
  60. package/taskmaster-docs/USER-GUIDE.md +84 -5
  61. package/templates/beagle/agents/admin/AGENTS.md +4 -2
  62. package/templates/taskmaster/agents/admin/AGENTS.md +1 -0
  63. package/dist/control-ui/assets/index-DtQHRIVD.css +0 -1
  64. 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
- "contact_lookup",
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, 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
+ }
@@ -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" && requested !== "elevenlabs" && requested !== "edge") {
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);
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "1.2.1",
3
- "commit": "238dd4da7156480a565ebe14908d0b24ded34899",
4
- "builtAt": "2026-02-24T15:34:23.973Z"
2
+ "version": "1.3.0",
3
+ "commit": "6432fa8c1f6f007fcdbee8123de84e0f8450d01c",
4
+ "builtAt": "2026-02-24T20:07:40.938Z"
5
5
  }
@@ -137,8 +137,7 @@ export function buildDefaultAgentList(workspaceRoot) {
137
137
  "session_status",
138
138
  "cron",
139
139
  "license_generate",
140
- "contact_lookup",
141
- "contact_update",
140
+ "group:contacts",
142
141
  "authorize_admin",
143
142
  "revoke_admin",
144
143
  "list_admins",
@@ -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");