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.
- package/README.md +50 -9
- package/bin/cli.js +2 -0
- package/bin/lynkr-init.js +4 -12
- package/bin/lynkr-reset.js +71 -0
- package/config/model-tiers.json +199 -52
- package/package.json +2 -2
- package/scripts/validate-difficulty-classifier.js +27 -6
- package/src/api/providers-handler.js +0 -1
- package/src/api/router.js +275 -160
- package/src/clients/databricks.js +95 -6
- package/src/config/index.js +3 -43
- package/src/orchestrator/index.js +117 -1235
- package/src/orchestrator/passthrough-stream.js +382 -0
- package/src/orchestrator/sse-transformer.js +408 -0
- package/src/routing/affinity-store.js +17 -3
- package/src/routing/difficulty-classifier.js +52 -10
- package/src/routing/index.js +35 -3
- package/src/routing/intent-score.js +20 -2
- package/src/routing/session-affinity.js +8 -1
- package/src/routing/side-channel-detector.js +103 -0
- package/src/server.js +0 -46
- package/src/agents/context-manager.js +0 -236
- package/src/agents/decomposition/dispatcher.js +0 -185
- package/src/agents/decomposition/gate.js +0 -136
- package/src/agents/decomposition/index.js +0 -183
- package/src/agents/decomposition/model-call.js +0 -75
- package/src/agents/decomposition/planner.js +0 -223
- package/src/agents/decomposition/synthesizer.js +0 -89
- package/src/agents/decomposition/telemetry.js +0 -55
- package/src/agents/definitions/loader.js +0 -653
- package/src/agents/executor.js +0 -457
- package/src/agents/index.js +0 -165
- package/src/agents/parallel-coordinator.js +0 -68
- package/src/agents/reflector.js +0 -331
- package/src/agents/skillbook.js +0 -331
- package/src/agents/store.js +0 -259
- package/src/edits/index.js +0 -171
- package/src/indexer/babel-parser.js +0 -213
- package/src/indexer/index.js +0 -1629
- package/src/indexer/navigation/index.js +0 -32
- package/src/indexer/navigation/providers/treeSitter.js +0 -36
- package/src/indexer/parser.js +0 -443
- package/src/tasks/store.js +0 -349
- package/src/tests/coverage.js +0 -173
- package/src/tests/index.js +0 -171
- package/src/tests/store.js +0 -213
- package/src/tools/agent-task.js +0 -145
- package/src/tools/code-mode.js +0 -304
- package/src/tools/decompose.js +0 -91
- package/src/tools/edits.js +0 -94
- package/src/tools/execution.js +0 -171
- package/src/tools/git.js +0 -1346
- package/src/tools/index.js +0 -306
- package/src/tools/indexer.js +0 -360
- package/src/tools/lazy-loader.js +0 -366
- package/src/tools/mcp-remote.js +0 -88
- package/src/tools/mcp.js +0 -116
- package/src/tools/process.js +0 -167
- package/src/tools/smart-selection.js +0 -180
- package/src/tools/stubs.js +0 -55
- package/src/tools/tasks.js +0 -260
- package/src/tools/tests.js +0 -132
- package/src/tools/tinyfish.js +0 -358
- package/src/tools/truncate.js +0 -106
- package/src/tools/web-client.js +0 -71
- package/src/tools/web.js +0 -415
- package/src/tools/workspace.js +0 -204
package/src/tools/process.js
DELETED
|
@@ -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
|
-
};
|
package/src/tools/stubs.js
DELETED
|
@@ -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
|
-
};
|
package/src/tools/tasks.js
DELETED
|
@@ -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
|
-
};
|