lynkr 9.9.1 → 9.10.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/README.md +50 -9
  2. package/bin/cli.js +2 -0
  3. package/bin/lynkr-init.js +4 -12
  4. package/bin/lynkr-reset.js +71 -0
  5. package/config/model-tiers.json +199 -52
  6. package/package.json +2 -2
  7. package/scripts/validate-difficulty-classifier.js +27 -6
  8. package/src/api/providers-handler.js +0 -1
  9. package/src/api/router.js +275 -160
  10. package/src/clients/databricks.js +95 -6
  11. package/src/config/index.js +3 -43
  12. package/src/orchestrator/index.js +117 -1235
  13. package/src/orchestrator/passthrough-stream.js +382 -0
  14. package/src/orchestrator/sse-transformer.js +408 -0
  15. package/src/routing/affinity-store.js +17 -3
  16. package/src/routing/difficulty-classifier.js +52 -10
  17. package/src/routing/index.js +35 -3
  18. package/src/routing/intent-score.js +20 -2
  19. package/src/routing/session-affinity.js +8 -1
  20. package/src/routing/side-channel-detector.js +103 -0
  21. package/src/server.js +0 -46
  22. package/src/agents/context-manager.js +0 -236
  23. package/src/agents/decomposition/dispatcher.js +0 -185
  24. package/src/agents/decomposition/gate.js +0 -136
  25. package/src/agents/decomposition/index.js +0 -183
  26. package/src/agents/decomposition/model-call.js +0 -75
  27. package/src/agents/decomposition/planner.js +0 -223
  28. package/src/agents/decomposition/synthesizer.js +0 -89
  29. package/src/agents/decomposition/telemetry.js +0 -55
  30. package/src/agents/definitions/loader.js +0 -653
  31. package/src/agents/executor.js +0 -457
  32. package/src/agents/index.js +0 -165
  33. package/src/agents/parallel-coordinator.js +0 -68
  34. package/src/agents/reflector.js +0 -331
  35. package/src/agents/skillbook.js +0 -331
  36. package/src/agents/store.js +0 -259
  37. package/src/edits/index.js +0 -171
  38. package/src/indexer/babel-parser.js +0 -213
  39. package/src/indexer/index.js +0 -1629
  40. package/src/indexer/navigation/index.js +0 -32
  41. package/src/indexer/navigation/providers/treeSitter.js +0 -36
  42. package/src/indexer/parser.js +0 -443
  43. package/src/tasks/store.js +0 -349
  44. package/src/tests/coverage.js +0 -173
  45. package/src/tests/index.js +0 -171
  46. package/src/tests/store.js +0 -213
  47. package/src/tools/agent-task.js +0 -145
  48. package/src/tools/code-mode.js +0 -304
  49. package/src/tools/decompose.js +0 -91
  50. package/src/tools/edits.js +0 -94
  51. package/src/tools/execution.js +0 -171
  52. package/src/tools/git.js +0 -1346
  53. package/src/tools/index.js +0 -306
  54. package/src/tools/indexer.js +0 -360
  55. package/src/tools/lazy-loader.js +0 -366
  56. package/src/tools/mcp-remote.js +0 -88
  57. package/src/tools/mcp.js +0 -116
  58. package/src/tools/process.js +0 -167
  59. package/src/tools/smart-selection.js +0 -180
  60. package/src/tools/stubs.js +0 -55
  61. package/src/tools/tasks.js +0 -260
  62. package/src/tools/tests.js +0 -132
  63. package/src/tools/tinyfish.js +0 -358
  64. package/src/tools/truncate.js +0 -106
  65. package/src/tools/web-client.js +0 -71
  66. package/src/tools/web.js +0 -415
  67. package/src/tools/workspace.js +0 -204
@@ -1,167 +0,0 @@
1
- const { spawn } = require("child_process");
2
- const path = require("path");
3
- const { workspaceRoot, resolveWorkspacePath } = require("../workspace");
4
- const { isSandboxEnabled, runSandboxProcess } = require("../mcp/sandbox");
5
-
6
- const DEFAULT_TIMEOUT_MS = 15000;
7
- const MAX_TIMEOUT_MS = 900000;
8
- const MAX_BUFFER_BYTES = 1024 * 1024; // 1MB
9
-
10
- function sanitiseEnv(env = {}) {
11
- const output = {};
12
- for (const [key, value] of Object.entries(env)) {
13
- if (typeof key !== "string") continue;
14
- if (value === undefined || value === null) continue;
15
- output[key] = typeof value === "string" ? value : String(value);
16
- }
17
- return output;
18
- }
19
-
20
- function normaliseTimeout(timeoutMs) {
21
- if (!Number.isFinite(timeoutMs)) return DEFAULT_TIMEOUT_MS;
22
- if (timeoutMs <= 0) return DEFAULT_TIMEOUT_MS;
23
- return Math.min(timeoutMs, MAX_TIMEOUT_MS);
24
- }
25
-
26
- function normaliseSandboxPreference(value) {
27
- switch (value) {
28
- case "always":
29
- case "never":
30
- case "auto":
31
- return value;
32
- default:
33
- return "never";
34
- }
35
- }
36
-
37
- async function runProcess({
38
- command,
39
- args = [],
40
- input,
41
- cwd,
42
- env,
43
- timeoutMs,
44
- maxBuffer = MAX_BUFFER_BYTES,
45
- shell = false,
46
- sandbox = "never",
47
- sessionId = null,
48
- }) {
49
- if (!command || typeof command !== "string") {
50
- throw new Error("Command must be a non-empty string.");
51
- }
52
- // cwd can be:
53
- // 1. An already-resolved absolute path (from normaliseCwd in execution.js)
54
- // 2. A relative path that needs resolving against workspaceRoot
55
- // 3. null/undefined (use workspaceRoot)
56
- let resolvedCwd;
57
- if (cwd) {
58
- // If it's already an absolute path, use it directly
59
- // Otherwise resolve against workspaceRoot
60
- if (path.isAbsolute(cwd)) {
61
- resolvedCwd = cwd;
62
- } else {
63
- resolvedCwd = resolveWorkspacePath(cwd);
64
- }
65
- } else {
66
- resolvedCwd = workspaceRoot;
67
- }
68
- const mergedEnv = { ...process.env, ...sanitiseEnv(env) };
69
- const timeout = normaliseTimeout(timeoutMs ?? DEFAULT_TIMEOUT_MS);
70
- const sandboxPreference = normaliseSandboxPreference(sandbox);
71
-
72
- if (sandboxPreference === "always" && !isSandboxEnabled()) {
73
- throw new Error("Sandbox execution requested but sandbox is not enabled.");
74
- }
75
-
76
- const shouldUseSandbox =
77
- sandboxPreference === "always" ||
78
- (sandboxPreference === "auto" && isSandboxEnabled());
79
-
80
- if (shouldUseSandbox) {
81
- return runSandboxProcess({
82
- sessionId,
83
- command,
84
- args,
85
- input,
86
- cwd: resolvedCwd,
87
- env: mergedEnv,
88
- timeoutMs: timeout,
89
- maxBuffer,
90
- });
91
- }
92
-
93
- return new Promise((resolve, reject) => {
94
- const child = spawn(command, args, {
95
- cwd: resolvedCwd,
96
- env: mergedEnv,
97
- shell,
98
- stdio: ["pipe", "pipe", "pipe"],
99
- });
100
-
101
- let stdout = "";
102
- let stderr = "";
103
- let stdoutOverflow = false;
104
- let stderrOverflow = false;
105
- const start = Date.now();
106
- let timedOut = false;
107
-
108
- const timer = setTimeout(() => {
109
- timedOut = true;
110
- child.kill("SIGKILL");
111
- }, timeout);
112
-
113
- const appendBuffer = (current, chunk) => {
114
- if (current.length >= maxBuffer) return { value: current, overflow: true };
115
- const next = current + chunk;
116
- if (next.length > maxBuffer) {
117
- return { value: next.slice(0, maxBuffer), overflow: true };
118
- }
119
- return { value: next, overflow: false };
120
- };
121
-
122
- child.stdout.on("data", (chunk) => {
123
- const { value, overflow } = appendBuffer(stdout, chunk.toString());
124
- stdout = value;
125
- if (overflow) stdoutOverflow = true;
126
- });
127
-
128
- child.stderr.on("data", (chunk) => {
129
- const { value, overflow } = appendBuffer(stderr, chunk.toString());
130
- stderr = value;
131
- if (overflow) stderrOverflow = true;
132
- });
133
-
134
- child.on("error", (err) => {
135
- clearTimeout(timer);
136
- reject(err);
137
- });
138
-
139
- child.on("close", (code, signal) => {
140
- clearTimeout(timer);
141
- resolve({
142
- exitCode: code,
143
- signal,
144
- stdout,
145
- stderr,
146
- stdoutOverflow,
147
- stderrOverflow,
148
- timedOut,
149
- durationMs: Date.now() - start,
150
- });
151
- });
152
-
153
- if (typeof input === "string" && child.stdin.writable) {
154
- child.stdin.write(input);
155
- child.stdin.end();
156
- } else if (!input && child.stdin.writable) {
157
- child.stdin.end();
158
- }
159
- });
160
- }
161
-
162
- module.exports = {
163
- runProcess,
164
- DEFAULT_TIMEOUT_MS,
165
- MAX_TIMEOUT_MS,
166
- MAX_BUFFER_BYTES,
167
- };
@@ -1,180 +0,0 @@
1
- /**
2
- * Smart Tool Selection — Conservative Stripping
3
- *
4
- * Strategy: instead of predicting which tools ARE needed (brittle regex),
5
- * only strip groups we are CERTAIN are irrelevant based on clear absence
6
- * of intent signals.
7
- *
8
- * Rules:
9
- * 1. Greeting → strip everything
10
- * 2. No write intent → strip Write / Edit / NotebookEdit
11
- * 3. No execution intent → strip Bash / KillShell
12
- * 4. No web intent → strip WebSearch / WebFetch
13
- *
14
- * File ops (Read, Grep, Glob) are NEVER stripped — they are the most
15
- * broadly useful and the most commonly needed unexpectedly.
16
- */
17
-
18
- const logger = require('../logger');
19
-
20
- const SYSTEM_REMINDER_PATTERN = /<system-reminder>[\s\S]*?<\/system-reminder>/g;
21
-
22
- // Clear greeting — strip all tools
23
- const GREETING_PATTERN = /^(hi|hello|hey|good morning|good afternoon|good evening|howdy|greetings|sup|yo)[\s\.\!\?]*$/i;
24
- const TECHNICAL_KEYWORDS = /code|function|class|file|module|import|export|async|await|promise|api|database|server|component|variable|array|object|\.[a-z]{1,5}\b|npm|git|docker|python|node|bash|run|install/i;
25
-
26
- // Intent signals — absence means we strip that group
27
- const WRITE_INTENT = /write|create\b|add to|update|modify|change|fix|delete|remove|insert|append|replace|save|edit|refactor|rename|move|reorganize|rewrite|implement|generate|produce|scaffold/i;
28
- const EXECUTE_INTENT = /run|execute|test|compile|build|deploy|start|install|launch|boot|npm|yarn|pnpm|git|python|node|docker|bash|sh\b|cmd|script|make|cargo|go run/i;
29
- const WEB_INTENT = /search online|search the web|search google|look up online|browse|website|https?:\/\//i;
30
-
31
- // Tools always kept (file search is never useless)
32
- const ALWAYS_KEEP = new Set([
33
- 'Read', 'Grep', 'Glob',
34
- 'Task', 'TaskOutput', 'TodoWrite', 'TodoRead',
35
- 'AskUserQuestion', 'Skill',
36
- 'EnterPlanMode', 'ExitPlanMode',
37
- ]);
38
-
39
- // Conditional strips: group → intent pattern that must be present to keep it
40
- const CONDITIONAL_GROUPS = [
41
- { names: ['Write', 'Edit', 'NotebookEdit'], intent: WRITE_INTENT },
42
- { names: ['Bash', 'KillShell'], intent: EXECUTE_INTENT },
43
- { names: ['WebSearch', 'WebFetch'], intent: WEB_INTENT },
44
- ];
45
-
46
- // Legacy map kept for telemetry label compatibility
47
- const TOOL_SELECTION_MAP = {
48
- conversational: [],
49
- simple_qa: [],
50
- file_reading: ['Read', 'Grep', 'Glob'],
51
- file_modification: ['Read', 'Write', 'Edit', 'Grep', 'Glob', 'Bash'],
52
- code_execution: ['Read', 'Write', 'Edit', 'Bash', 'Grep', 'Glob'],
53
- coding: ['Read', 'Write', 'Edit', 'Bash', 'Grep', 'Glob'],
54
- research: ['Read', 'Grep', 'Glob', 'WebSearch', 'WebFetch'],
55
- complex_task: ['Read', 'Write', 'Edit', 'Bash', 'Grep', 'Glob', 'WebSearch', 'WebFetch', 'Task', 'TodoWrite', 'AskUserQuestion'],
56
- };
57
-
58
- // ─── Helpers ──────────────────────────────────────────────────────────────────
59
-
60
- function getLastUserContent(payload) {
61
- if (!Array.isArray(payload.messages)) return '';
62
- for (let i = payload.messages.length - 1; i >= 0; i--) {
63
- const msg = payload.messages[i];
64
- if (msg?.role !== 'user') continue;
65
- let text = '';
66
- if (typeof msg.content === 'string') {
67
- text = msg.content;
68
- } else if (Array.isArray(msg.content)) {
69
- text = msg.content.filter(b => b?.type === 'text').map(b => b.text || '').join(' ');
70
- }
71
- return text.replace(SYSTEM_REMINDER_PATTERN, '').trim();
72
- }
73
- return '';
74
- }
75
-
76
- function isGreeting(content) {
77
- const t = content.trim();
78
- return GREETING_PATTERN.test(t) || (t.length < 20 && !TECHNICAL_KEYWORDS.test(t));
79
- }
80
-
81
- // ─── Classifier (conservative) ───────────────────────────────────────────────
82
-
83
- /**
84
- * Classify request and compute which tool groups to strip.
85
- * Returns a classification object for logging/telemetry compatibility.
86
- */
87
- function classifyRequestType(payload) {
88
- const content = getLastUserContent(payload);
89
- const lower = content.toLowerCase();
90
- const msgCount = payload.messages?.length ?? 0;
91
-
92
- // Greeting → strip everything
93
- if (isGreeting(lower)) {
94
- return { type: 'conversational', confidence: 1.0, keywords: ['greeting'], _stripped: ['Write', 'Edit', 'NotebookEdit', 'Bash', 'KillShell', 'WebSearch', 'WebFetch'] };
95
- }
96
-
97
- const stripped = [];
98
- for (const { names, intent } of CONDITIONAL_GROUPS) {
99
- if (!intent.test(lower)) stripped.push(...names);
100
- }
101
-
102
- // Derive a label for telemetry
103
- const hasWrite = WRITE_INTENT.test(lower);
104
- const hasExec = EXECUTE_INTENT.test(lower);
105
- const hasWeb = WEB_INTENT.test(lower);
106
-
107
- const type = hasWrite || hasExec ? 'file_modification'
108
- : hasWeb ? 'research'
109
- : msgCount > 10 ? 'complex_task'
110
- : 'file_reading';
111
-
112
- return { type, confidence: 0.9, keywords: ['conservative'], _stripped: stripped };
113
- }
114
-
115
- // ─── Tool filter ─────────────────────────────────────────────────────────────
116
-
117
- function estimateToolTokens(tools) {
118
- if (!Array.isArray(tools)) return 0;
119
- return tools.length * 175;
120
- }
121
-
122
- /**
123
- * Apply conservative stripping to the tool list.
124
- */
125
- function recordStrippingSavings(before, after) {
126
- if (after >= before) return;
127
- try {
128
- const telemetry = require('../routing/telemetry');
129
- telemetry.recordSavings('tool_stripping', (before - after) * 175);
130
- } catch { /* telemetry is best-effort */ }
131
- }
132
-
133
- function selectToolsSmartly(tools, classification, options = {}) {
134
- if (!Array.isArray(tools) || tools.length === 0) return tools;
135
-
136
- const { provider = 'databricks' } = options;
137
- const strippedNames = new Set(classification._stripped ?? []);
138
-
139
- // Greeting: strip everything
140
- if (classification.type === 'conversational') {
141
- recordStrippingSavings(tools.length, 0);
142
- return [];
143
- }
144
-
145
- // Strip only the flagged groups; always keep ALWAYS_KEEP tools
146
- let selected = tools.filter(tool => {
147
- const name = String(tool.name || '');
148
- if (ALWAYS_KEEP.has(name)) return true;
149
- return !strippedNames.has(name);
150
- });
151
-
152
- // Safety: if we somehow stripped everything, return full list
153
- if (selected.length === 0) return tools;
154
-
155
- // Code Mode meta-tools always included
156
- const codeConfig = require('../config');
157
- if (codeConfig.mcp?.codeMode?.enabled) {
158
- const codeModeNames = new Set(['mcp_list_tools', 'mcp_tool_info', 'mcp_tool_docs', 'mcp_execute']);
159
- for (const tool of tools) {
160
- if (codeModeNames.has(tool.name) && !selected.some(t => t.name === tool.name)) {
161
- selected.push(tool);
162
- }
163
- }
164
- }
165
-
166
- // Ollama has a smaller context — cap at 10 tools
167
- if (provider === 'ollama' && selected.length > 10) {
168
- selected = selected.slice(0, 10);
169
- }
170
-
171
- recordStrippingSavings(tools.length, selected.length);
172
- return selected;
173
- }
174
-
175
- module.exports = {
176
- classifyRequestType,
177
- selectToolsSmartly,
178
- estimateToolTokens,
179
- TOOL_SELECTION_MAP,
180
- };
@@ -1,55 +0,0 @@
1
- const { registerTool, hasTool } = require(".");
2
-
3
- const STUB_TOOLS = [
4
- {
5
- name: "fs_read",
6
- description: "Read file contents from the active workspace.",
7
- },
8
- {
9
- name: "fs_write",
10
- description: "Write or create files within the active workspace.",
11
- },
12
- {
13
- name: "edit_patch",
14
- description: "Apply unified diff patches to workspace files.",
15
- },
16
- {
17
- name: "shell",
18
- description: "Execute shell commands inside the workspace sandbox.",
19
- },
20
- {
21
- name: "python_exec",
22
- description: "Run Python snippets in the managed runtime.",
23
- },
24
- {
25
- name: "web_search",
26
- description: "Perform a web search and return summarized results.",
27
- },
28
- ];
29
-
30
- function createStubHandler(name, description) {
31
- return async ({ args }) => ({
32
- ok: false,
33
- status: 501,
34
- content: {
35
- error: "tool_not_implemented",
36
- tool: name,
37
- description,
38
- input: args,
39
- hint: "This is a stub tool. Implement a real handler to enable this capability.",
40
- },
41
- });
42
- }
43
-
44
- function registerStubTools() {
45
- STUB_TOOLS.forEach((tool) => {
46
- if (!hasTool(tool.name)) {
47
- registerTool(tool.name, createStubHandler(tool.name, tool.description), tool);
48
- }
49
- });
50
- }
51
-
52
- module.exports = {
53
- STUB_TOOLS,
54
- registerStubTools,
55
- };
@@ -1,260 +0,0 @@
1
- const {
2
- createTask,
3
- listTasks,
4
- getTaskById,
5
- updateTask,
6
- deleteTask,
7
- setTaskStatus,
8
- } = require("../tasks/store");
9
- const { registerTool } = require(".");
10
-
11
- function normaliseTagsInput(value) {
12
- if (value === undefined) return undefined;
13
- if (Array.isArray(value)) {
14
- return value
15
- .map((item) => (typeof item === "string" ? item.trim() : null))
16
- .filter((item) => item && item.length > 0);
17
- }
18
- if (typeof value === "string") {
19
- return value
20
- .split(",")
21
- .map((item) => item.trim())
22
- .filter((item) => item.length > 0);
23
- }
24
- return undefined;
25
- }
26
-
27
- function normaliseMetadataInput(value) {
28
- if (value === undefined) return undefined;
29
- if (value === null) return null;
30
- if (typeof value === "object") return value;
31
- if (typeof value === "string") {
32
- try {
33
- return JSON.parse(value);
34
- } catch {
35
- return { note: value };
36
- }
37
- }
38
- return undefined;
39
- }
40
-
41
- function parseLimit(limit) {
42
- if (limit === undefined || limit === null) return undefined;
43
- const num = Number(limit);
44
- if (!Number.isFinite(num)) return undefined;
45
- const int = Math.trunc(num);
46
- if (int <= 0) return undefined;
47
- return Math.min(int, 500);
48
- }
49
-
50
- function registerTaskTools() {
51
- registerTool(
52
- "workspace_tasks_list",
53
- async ({ args = {} }) => {
54
- const tasks = listTasks({
55
- status: typeof args.status === "string" ? args.status.trim() : undefined,
56
- linkedFile:
57
- typeof args.file === "string"
58
- ? args.file
59
- : typeof args.path === "string"
60
- ? args.path
61
- : undefined,
62
- search: typeof args.search === "string" ? args.search : undefined,
63
- limit: parseLimit(args.limit),
64
- orderBy: typeof args.order === "string" ? args.order : undefined,
65
- });
66
-
67
- return {
68
- ok: true,
69
- status: 200,
70
- content: JSON.stringify(
71
- {
72
- tasks,
73
- total: tasks.length,
74
- },
75
- null,
76
- 2,
77
- ),
78
- metadata: {
79
- count: tasks.length,
80
- },
81
- };
82
- },
83
- { category: "tasks" },
84
- );
85
-
86
- registerTool(
87
- "workspace_task_get",
88
- async ({ args = {} }) => {
89
- const id = Number.parseInt(args.id ?? args.task_id ?? args.taskId, 10);
90
- if (!Number.isFinite(id)) {
91
- throw new Error("workspace_task_get requires a numeric id.");
92
- }
93
- const task = getTaskById(id);
94
- if (!task) {
95
- return {
96
- ok: false,
97
- status: 404,
98
- content: JSON.stringify(
99
- {
100
- error: "task_not_found",
101
- id,
102
- },
103
- null,
104
- 2,
105
- ),
106
- metadata: {
107
- id,
108
- found: false,
109
- },
110
- };
111
- }
112
- return {
113
- ok: true,
114
- status: 200,
115
- content: JSON.stringify(task, null, 2),
116
- metadata: {
117
- id,
118
- },
119
- };
120
- },
121
- { category: "tasks" },
122
- );
123
-
124
- registerTool(
125
- "workspace_task_create",
126
- async ({ args = {} }, context = {}) => {
127
- const title = args.title ?? args.name ?? args.summary;
128
- if (typeof title !== "string" || !title.trim()) {
129
- throw new Error("workspace_task_create requires a title.");
130
- }
131
-
132
- const tags = normaliseTagsInput(args.tags);
133
- const metadata = normaliseMetadataInput(args.metadata);
134
- const task = createTask({
135
- title,
136
- status: typeof args.status === "string" ? args.status.trim() : "todo",
137
- priority:
138
- Number.isFinite(args.priority) || Number.isFinite(Number(args.priority))
139
- ? Number(args.priority)
140
- : 0,
141
- tags,
142
- linkedFile:
143
- typeof args.file === "string"
144
- ? args.file
145
- : typeof args.path === "string"
146
- ? args.path
147
- : null,
148
- createdBy: context.session?.id ?? context.sessionId ?? null,
149
- metadata,
150
- });
151
-
152
- return {
153
- ok: true,
154
- status: 201,
155
- content: JSON.stringify(task, null, 2),
156
- metadata: {
157
- id: task.id,
158
- },
159
- };
160
- },
161
- { category: "tasks" },
162
- );
163
-
164
- registerTool(
165
- "workspace_task_update",
166
- async ({ args = {} }, context = {}) => {
167
- const id = Number.parseInt(args.id ?? args.task_id ?? args.taskId, 10);
168
- if (!Number.isFinite(id)) {
169
- throw new Error("workspace_task_update requires a numeric id.");
170
- }
171
-
172
- const updates = {};
173
- if (args.title !== undefined) updates.title = args.title;
174
- if (args.status !== undefined) updates.status = args.status;
175
- if (args.priority !== undefined) updates.priority = Number(args.priority);
176
- const tags = normaliseTagsInput(args.tags);
177
- if (tags !== undefined) updates.tags = tags;
178
- if (args.file !== undefined || args.path !== undefined) {
179
- updates.linkedFile =
180
- typeof args.file === "string"
181
- ? args.file
182
- : typeof args.path === "string"
183
- ? args.path
184
- : null;
185
- }
186
- const metadata = normaliseMetadataInput(args.metadata);
187
- if (metadata !== undefined) updates.metadata = metadata;
188
-
189
- updates.updatedBy = context.session?.id ?? context.sessionId ?? null;
190
-
191
- const task = updateTask(id, updates);
192
- return {
193
- ok: true,
194
- status: 200,
195
- content: JSON.stringify(task, null, 2),
196
- metadata: {
197
- id: task.id,
198
- },
199
- };
200
- },
201
- { category: "tasks" },
202
- );
203
-
204
- registerTool(
205
- "workspace_task_set_status",
206
- async ({ args = {} }, context = {}) => {
207
- const id = Number.parseInt(args.id ?? args.task_id ?? args.taskId, 10);
208
- if (!Number.isFinite(id)) {
209
- throw new Error("workspace_task_set_status requires a numeric id.");
210
- }
211
- const status = args.status ?? args.state;
212
- if (typeof status !== "string" || !status.trim()) {
213
- throw new Error("workspace_task_set_status requires a non-empty status.");
214
- }
215
- const task = setTaskStatus(id, status.trim(), context.session?.id ?? context.sessionId ?? null);
216
- return {
217
- ok: true,
218
- status: 200,
219
- content: JSON.stringify(task, null, 2),
220
- metadata: {
221
- id: task.id,
222
- status: task.status,
223
- },
224
- };
225
- },
226
- { category: "tasks" },
227
- );
228
-
229
- registerTool(
230
- "workspace_task_delete",
231
- async ({ args = {} }) => {
232
- const id = Number.parseInt(args.id ?? args.task_id ?? args.taskId, 10);
233
- if (!Number.isFinite(id)) {
234
- throw new Error("workspace_task_delete requires a numeric id.");
235
- }
236
- const deleted = deleteTask(id);
237
- return {
238
- ok: deleted,
239
- status: deleted ? 200 : 404,
240
- content: JSON.stringify(
241
- {
242
- id,
243
- deleted,
244
- },
245
- null,
246
- 2,
247
- ),
248
- metadata: {
249
- id,
250
- deleted,
251
- },
252
- };
253
- },
254
- { category: "tasks" },
255
- );
256
- }
257
-
258
- module.exports = {
259
- registerTaskTools,
260
- };