@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
@@ -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.0",
3
- "commit": "8cc810fa3a5317aec897706a333f431ee3e61995",
4
- "builtAt": "2026-02-24T14:07:31.816Z"
2
+ "version": "1.3.0",
3
+ "commit": "6432fa8c1f6f007fcdbee8123de84e0f8450d01c",
4
+ "builtAt": "2026-02-24T20:07:40.938Z"
5
5
  }
@@ -134,12 +134,10 @@ export function buildDefaultAgentList(workspaceRoot) {
134
134
  "current_time",
135
135
  "sessions_list",
136
136
  "sessions_history",
137
- "sessions_send",
138
137
  "session_status",
139
138
  "cron",
140
139
  "license_generate",
141
- "contact_lookup",
142
- "contact_update",
140
+ "group:contacts",
143
141
  "authorize_admin",
144
142
  "revoke_admin",
145
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");
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Individual contact tool names that should be replaced by `group:contacts`.
3
+ * Order does not matter — all are removed and replaced with the group.
4
+ */
5
+ const INDIVIDUAL_CONTACT_TOOLS = [
6
+ "contact_create",
7
+ "contact_delete",
8
+ "contact_lookup",
9
+ "contact_update",
10
+ ];
11
+ function isAdminAgent(agent) {
12
+ const id = agent.id?.trim() ?? "";
13
+ return id === "admin" || id.endsWith("-admin");
14
+ }
15
+ /**
16
+ * Ensure admin agents use `group:contacts` instead of individual contact tools.
17
+ *
18
+ * Runs unconditionally on gateway startup (like `applyPluginAutoEnable`).
19
+ * Idempotent — if the allow list already contains `group:contacts`, no changes.
20
+ */
21
+ export function reconcileAgentContactTools(params) {
22
+ const config = structuredClone(params.config);
23
+ const changes = [];
24
+ const agents = config.agents?.list;
25
+ if (!Array.isArray(agents))
26
+ return { config, changes };
27
+ for (const agent of agents) {
28
+ if (!agent || !isAdminAgent(agent))
29
+ continue;
30
+ const allow = agent.tools?.allow;
31
+ if (!Array.isArray(allow))
32
+ continue;
33
+ // Already using the group — nothing to do
34
+ if (allow.includes("group:contacts"))
35
+ continue;
36
+ // Check if any individual contact tools are present
37
+ const hasAny = INDIVIDUAL_CONTACT_TOOLS.some((t) => allow.includes(t));
38
+ if (!hasAny)
39
+ continue;
40
+ // Remove individual entries, add group
41
+ const removed = [];
42
+ for (const tool of INDIVIDUAL_CONTACT_TOOLS) {
43
+ const idx = allow.indexOf(tool);
44
+ if (idx !== -1) {
45
+ allow.splice(idx, 1);
46
+ removed.push(tool);
47
+ }
48
+ }
49
+ allow.push("group:contacts");
50
+ changes.push(`Replaced ${removed.join(", ")} with group:contacts in agent "${agent.id}" tools.allow.`);
51
+ }
52
+ return { config, changes };
53
+ }
@@ -1,6 +1,6 @@
1
1
  import { DEFAULT_CONTEXT_TOKENS } from "../agents/defaults.js";
2
2
  import { parseModelRef } from "../agents/model-selection.js";
3
- import { upsertAuthProfile } from "../agents/auth-profiles.js";
3
+ import { upsertAuthProfile, removeAuthProfile } from "../agents/auth-profiles.js";
4
4
  import { resolveTalkApiKey } from "./talk.js";
5
5
  import { DEFAULT_AGENT_MAX_CONCURRENT, DEFAULT_SUBAGENT_MAX_CONCURRENT } from "./agent-limits.js";
6
6
  let defaultWarnState = { warned: false };
@@ -372,10 +372,19 @@ export function applyApiKeys(config) {
372
372
  const keys = config.apiKeys;
373
373
  if (!keys || Object.keys(keys).length === 0)
374
374
  return config;
375
+ const disabled = config.apiKeysDisabled ?? {};
375
376
  let cfg = config;
376
377
  for (const [provider, key] of Object.entries(keys)) {
377
378
  if (!key?.trim())
378
379
  continue;
380
+ if (disabled[provider]) {
381
+ // Actively remove auth profiles for disabled model providers so stale
382
+ // credentials from a previous config load don't persist in memory.
383
+ if (API_KEY_MODEL_PROVIDERS.has(provider)) {
384
+ removeAuthProfile({ profileId: `${provider}:api-key` });
385
+ }
386
+ continue;
387
+ }
379
388
  const trimmedKey = key.trim();
380
389
  if (API_KEY_MODEL_PROVIDERS.has(provider)) {
381
390
  upsertAuthProfile({
@@ -216,4 +216,30 @@ export const LEGACY_CONFIG_MIGRATIONS_PART_3 = [
216
216
  }
217
217
  },
218
218
  },
219
+ {
220
+ id: "agents-tools-remove-sessions_send",
221
+ describe: "Remove sessions_send from agent tools.allow (tool removed for security)",
222
+ apply: (raw, changes) => {
223
+ const agents = getRecord(raw.agents);
224
+ const list = getAgentsList(agents);
225
+ for (const entry of list) {
226
+ if (!isRecord(entry))
227
+ continue;
228
+ const id = typeof entry.id === "string" ? entry.id.trim() : "";
229
+ if (!id)
230
+ continue;
231
+ const tools = getRecord(entry.tools);
232
+ if (!tools)
233
+ continue;
234
+ if (!Array.isArray(tools.allow))
235
+ continue;
236
+ const allow = tools.allow;
237
+ const index = allow.indexOf("sessions_send");
238
+ if (index !== -1) {
239
+ allow.splice(index, 1);
240
+ changes.push(`Removed sessions_send from agent "${id}" tools.allow.`);
241
+ }
242
+ }
243
+ },
244
+ },
219
245
  ];
@@ -138,7 +138,7 @@ export const MarkdownConfigSchema = z
138
138
  })
139
139
  .strict()
140
140
  .optional();
141
- export const TtsProviderSchema = z.enum(["elevenlabs", "openai", "edge"]);
141
+ export const TtsProviderSchema = z.enum(["elevenlabs", "openai", "hume", "edge"]);
142
142
  export const TtsModeSchema = z.enum(["final", "all"]);
143
143
  export const TtsAutoSchema = z.enum(["off", "always", "inbound", "tagged"]);
144
144
  export const TtsConfigSchema = z
@@ -191,6 +191,14 @@ export const TtsConfigSchema = z
191
191
  })
192
192
  .strict()
193
193
  .optional(),
194
+ hume: z
195
+ .object({
196
+ apiKey: z.string().optional(),
197
+ voice: z.string().optional(),
198
+ description: z.string().optional(),
199
+ })
200
+ .strict()
201
+ .optional(),
194
202
  edge: z
195
203
  .object({
196
204
  enabled: z.boolean().optional(),
@@ -552,6 +552,7 @@ export const TaskmasterSchema = z
552
552
  .strict())
553
553
  .optional(),
554
554
  apiKeys: z.record(z.string(), z.string()).optional(),
555
+ apiKeysDisabled: z.record(z.string(), z.boolean()).optional(),
555
556
  access: z
556
557
  .object({
557
558
  masterPin: z