dr-claw 1.1.3 → 1.1.4
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/dist/assets/{_basePickBy-CTZ7OJiJ.js → _basePickBy-7CEpiVqc.js} +1 -1
- package/dist/assets/{_baseUniq-LYG5erFT.js → _baseUniq-yh8GoKah.js} +1 -1
- package/dist/assets/{arc-CYhqYtN8.js → arc-xxYot-So.js} +1 -1
- package/dist/assets/{architectureDiagram-VXUJARFQ-B7xb9vYd.js → architectureDiagram-VXUJARFQ-Bb1NqOFB.js} +1 -1
- package/dist/assets/{blockDiagram-VD42YOAC-Dz6eN3Yc.js → blockDiagram-VD42YOAC-D8xMHk8C.js} +1 -1
- package/dist/assets/{c4Diagram-YG6GDRKO-DIa9rHl5.js → c4Diagram-YG6GDRKO-69UiBMVa.js} +1 -1
- package/dist/assets/channel-BJdHHkhR.js +1 -0
- package/dist/assets/{chunk-4BX2VUAB-JZVAPeYL.js → chunk-4BX2VUAB-DXrPhE6K.js} +1 -1
- package/dist/assets/{chunk-55IACEB6-BoNiAnP-.js → chunk-55IACEB6-B3sw4AGX.js} +1 -1
- package/dist/assets/{chunk-B4BG7PRW-BURhBXxZ.js → chunk-B4BG7PRW-Dd4n17kR.js} +1 -1
- package/dist/assets/{chunk-DI55MBZ5-Bxa-FsXa.js → chunk-DI55MBZ5-DuTb9gb8.js} +1 -1
- package/dist/assets/{chunk-FMBD7UC4-Dn_6iLa4.js → chunk-FMBD7UC4-CGErQ-ud.js} +1 -1
- package/dist/assets/{chunk-QN33PNHL-B0N2-K2K.js → chunk-QN33PNHL-Bk5fuYbR.js} +1 -1
- package/dist/assets/{chunk-QZHKN3VN-Bd7csSYW.js → chunk-QZHKN3VN-2UdZTbge.js} +1 -1
- package/dist/assets/{chunk-TZMSLE5B-DV5aJTr6.js → chunk-TZMSLE5B-Bl-UYKEK.js} +1 -1
- package/dist/assets/{classDiagram-2ON5EDUG-iZL_J_xq.js → classDiagram-2ON5EDUG-EaoBDWqJ.js} +1 -1
- package/dist/assets/{classDiagram-v2-WZHVMYZB-iZL_J_xq.js → classDiagram-v2-WZHVMYZB-EaoBDWqJ.js} +1 -1
- package/dist/assets/clone-CwXTbwxl.js +1 -0
- package/dist/assets/{cose-bilkent-S5V4N54A-C_Uhc5Rn.js → cose-bilkent-S5V4N54A-qXNKLX2R.js} +1 -1
- package/dist/assets/{dagre-6UL2VRFP-BiW_P9KL.js → dagre-6UL2VRFP-DmHvDWep.js} +1 -1
- package/dist/assets/{diagram-PSM6KHXK-uGQgzEbt.js → diagram-PSM6KHXK-C_Rj4DX3.js} +1 -1
- package/dist/assets/{diagram-QEK2KX5R-qox_ZDR7.js → diagram-QEK2KX5R-BVr3mPXW.js} +1 -1
- package/dist/assets/{diagram-S2PKOQOG-RKFFDPoU.js → diagram-S2PKOQOG-DVx6i0Qe.js} +1 -1
- package/dist/assets/{erDiagram-Q2GNP2WA-11Go9j0k.js → erDiagram-Q2GNP2WA-BJQyv9_t.js} +1 -1
- package/dist/assets/{flowDiagram-NV44I4VS-DBAiHvAx.js → flowDiagram-NV44I4VS-DCR8sPAE.js} +1 -1
- package/dist/assets/{ganttDiagram-JELNMOA3-Dzc11a-7.js → ganttDiagram-JELNMOA3-BcXFAthK.js} +1 -1
- package/dist/assets/{gitGraphDiagram-V2S2FVAM-DJVXfOqP.js → gitGraphDiagram-V2S2FVAM-CnS56w1x.js} +1 -1
- package/dist/assets/{graph-CJ95S3XQ.js → graph-DGtZXuDY.js} +1 -1
- package/dist/assets/index-C-sL-ag0.css +1 -0
- package/dist/assets/index-VdqxIFPL.js +670 -0
- package/dist/assets/{infoDiagram-HS3SLOUP-DpdkGgBK.js → infoDiagram-HS3SLOUP-DtI272ZD.js} +1 -1
- package/dist/assets/{journeyDiagram-XKPGCS4Q-CSGpNz7D.js → journeyDiagram-XKPGCS4Q-DHp_Kica.js} +1 -1
- package/dist/assets/{kanban-definition-3W4ZIXB7-BvPB3vMM.js → kanban-definition-3W4ZIXB7-CjgHMNku.js} +1 -1
- package/dist/assets/{layout-C96BbhFQ.js → layout-Cnc-DCSb.js} +1 -1
- package/dist/assets/{linear-HjDfr5-H.js → linear-BtRgmV19.js} +1 -1
- package/dist/assets/{mermaid.core-J7RjAKTD.js → mermaid.core-B20uzv6N.js} +5 -5
- package/dist/assets/{mindmap-definition-VGOIOE7T-C6MlEWCC.js → mindmap-definition-VGOIOE7T-bbcLXkWp.js} +1 -1
- package/dist/assets/{pieDiagram-ADFJNKIX-Bi5BLoNG.js → pieDiagram-ADFJNKIX-B4fEzWKO.js} +1 -1
- package/dist/assets/{quadrantDiagram-AYHSOK5B-Bo14V3Q1.js → quadrantDiagram-AYHSOK5B-B5G0SMbK.js} +1 -1
- package/dist/assets/{requirementDiagram-UZGBJVZJ-CP3_EwJ4.js → requirementDiagram-UZGBJVZJ-DsqtOFgj.js} +1 -1
- package/dist/assets/{sankeyDiagram-TZEHDZUN-B3wFfI_W.js → sankeyDiagram-TZEHDZUN-CiGemFRE.js} +1 -1
- package/dist/assets/{sequenceDiagram-WL72ISMW-fZVhVt_s.js → sequenceDiagram-WL72ISMW-hEMvYbe9.js} +1 -1
- package/dist/assets/{stateDiagram-FKZM4ZOC-BooCitAX.js → stateDiagram-FKZM4ZOC-Cn8VB7b1.js} +1 -1
- package/dist/assets/stateDiagram-v2-4FDKWEC3-DaH0PZOq.js +1 -0
- package/dist/assets/{timeline-definition-IT6M3QCI-CM0zUx-Q.js → timeline-definition-IT6M3QCI-DChT1GUr.js} +1 -1
- package/dist/assets/{treemap-GDKQZRPO-B-8ph3e8.js → treemap-GDKQZRPO-Dflv6a8z.js} +1 -1
- package/dist/assets/{xychartDiagram-PRI3JC2R-Q2k0EFpn.js → xychartDiagram-PRI3JC2R-Dr4urXR8.js} +1 -1
- package/dist/icons/nanoclaude.png +0 -0
- package/dist/index.html +2 -2
- package/package.json +1 -1
- package/public/icons/nanoclaude.png +0 -0
- package/scripts/native-runtime.mjs +5 -3
- package/server/__tests__/gemini-api.test.mjs +699 -0
- package/server/__tests__/gemini-session-index.test.mjs +95 -0
- package/server/claude-sdk.js +82 -3
- package/server/cli-chat.js +28 -17
- package/server/cli.js +13 -1
- package/server/data/news-log-arxiv.json +1 -0
- package/server/data/news-log-huggingface.json +1 -0
- package/server/data/news-results-arxiv.json +460 -0
- package/server/data/news-results-huggingface.json +817 -0
- package/server/data/research_interests_arxiv.json +50 -0
- package/server/data/research_interests_huggingface.json +4 -0
- package/server/database/db.js +92 -0
- package/server/database/init.sql +17 -1
- package/server/gemini-api.js +1838 -0
- package/server/gemini-cli.js +13 -2
- package/server/index.js +94 -17
- package/server/load-env.js +13 -1
- package/server/local-gpu.js +3 -1
- package/server/nano-claude-code.js +340 -0
- package/server/nanoSessionPaths.js +41 -0
- package/server/openai-codex.js +6 -2
- package/server/openrouter.js +37 -22
- package/server/projects.js +207 -0
- package/server/routes/agent.js +32 -7
- package/server/routes/auto-research.js +1 -1
- package/server/routes/claude-btw.js +55 -0
- package/server/routes/cli-auth.js +103 -2
- package/server/routes/commands.js +25 -1
- package/server/routes/community-tools.js +26 -21
- package/server/routes/memory.js +143 -0
- package/server/routes/news.js +3 -0
- package/server/routes/skills.js +35 -0
- package/server/routes/user.js +3 -5
- package/server/utils/__tests__/safePath.test.js +93 -0
- package/server/utils/cliResolution.js +13 -1
- package/server/utils/geminiOAuth.js +333 -0
- package/server/utils/memoryPrompt.js +78 -0
- package/server/utils/safePath.js +51 -0
- package/server/utils/sessionIndex.js +2 -0
- package/server/utils/skillExpander.js +142 -0
- package/server/utils/spawnAsync.js +78 -0
- package/shared/modelConstants.js +19 -1
- package/dist/assets/channel-Dk-4X8XT.js +0 -1
- package/dist/assets/clone-DbPu_xVW.js +0 -1
- package/dist/assets/index-1MRmplD1.css +0 -1
- package/dist/assets/index-CFbkRzqw.js +0 -664
- package/dist/assets/stateDiagram-v2-4FDKWEC3-BzH7qQMD.js +0 -1
|
@@ -25,6 +25,9 @@ const PROVIDER_INSTALLERS = {
|
|
|
25
25
|
darwin: [
|
|
26
26
|
{ bin: 'brew', args: ['install', 'claude-code'], label: 'brew install claude-code' },
|
|
27
27
|
],
|
|
28
|
+
linux: [
|
|
29
|
+
{ bin: 'npm', args: ['install', '-g', '@anthropic-ai/claude-code'], label: 'npm install -g @anthropic-ai/claude-code' },
|
|
30
|
+
],
|
|
28
31
|
win32: [
|
|
29
32
|
{ bin: 'winget', args: ['install', '--id', 'Anthropic.ClaudeCode', '-e'], label: 'winget install Anthropic.ClaudeCode' },
|
|
30
33
|
],
|
|
@@ -51,6 +54,9 @@ const PROVIDER_INSTALLERS = {
|
|
|
51
54
|
darwin: [
|
|
52
55
|
{ bin: 'npm', args: ['install', '-g', '@openai/codex'], label: 'npm install -g @openai/codex' },
|
|
53
56
|
],
|
|
57
|
+
linux: [
|
|
58
|
+
{ bin: 'npm', args: ['install', '-g', '@openai/codex'], label: 'npm install -g @openai/codex' },
|
|
59
|
+
],
|
|
54
60
|
win32: [
|
|
55
61
|
{ bin: 'npm.cmd', args: ['install', '-g', '@openai/codex'], label: 'npm install -g @openai/codex' },
|
|
56
62
|
],
|
|
@@ -64,11 +70,42 @@ const PROVIDER_INSTALLERS = {
|
|
|
64
70
|
darwin: [
|
|
65
71
|
{ bin: 'npm', args: ['install', '-g', '@google/gemini-cli'], label: 'npm install -g @google/gemini-cli' },
|
|
66
72
|
],
|
|
73
|
+
linux: [
|
|
74
|
+
{ bin: 'npm', args: ['install', '-g', '@google/gemini-cli'], label: 'npm install -g @google/gemini-cli' },
|
|
75
|
+
],
|
|
67
76
|
win32: [
|
|
68
77
|
{ bin: 'npm.cmd', args: ['install', '-g', '@google/gemini-cli'], label: 'npm install -g @google/gemini-cli' },
|
|
69
78
|
],
|
|
70
79
|
},
|
|
71
80
|
},
|
|
81
|
+
nano: {
|
|
82
|
+
displayName: 'Nano Claude Code',
|
|
83
|
+
docsUrl: 'https://github.com/OpenLAIR/nano-claude-code',
|
|
84
|
+
fallbackDownloadUrl: 'https://github.com/OpenLAIR/nano-claude-code',
|
|
85
|
+
commands: {
|
|
86
|
+
darwin: [
|
|
87
|
+
{
|
|
88
|
+
bin: 'pip3',
|
|
89
|
+
args: ['install', '-U', 'git+https://github.com/OpenLAIR/nano-claude-code.git'],
|
|
90
|
+
label: 'pip3 install from GitHub (recommended)',
|
|
91
|
+
},
|
|
92
|
+
],
|
|
93
|
+
linux: [
|
|
94
|
+
{
|
|
95
|
+
bin: 'pip3',
|
|
96
|
+
args: ['install', '-U', 'git+https://github.com/OpenLAIR/nano-claude-code.git'],
|
|
97
|
+
label: 'pip3 install from GitHub (use a venv if your distro manages system Python)',
|
|
98
|
+
},
|
|
99
|
+
],
|
|
100
|
+
win32: [
|
|
101
|
+
{
|
|
102
|
+
bin: 'pip',
|
|
103
|
+
args: ['install', '-U', 'git+https://github.com/OpenLAIR/nano-claude-code.git'],
|
|
104
|
+
label: 'pip install from GitHub',
|
|
105
|
+
},
|
|
106
|
+
],
|
|
107
|
+
},
|
|
108
|
+
},
|
|
72
109
|
};
|
|
73
110
|
|
|
74
111
|
function buildCliInstallHint(agent) {
|
|
@@ -88,6 +125,8 @@ function buildCliInstallHint(agent) {
|
|
|
88
125
|
return 'Gemini CLI is not installed. Install it first, then retry login.';
|
|
89
126
|
case 'openrouter':
|
|
90
127
|
return 'Set OPENROUTER_API_KEY in your .env file. Get a key at https://openrouter.ai/keys';
|
|
128
|
+
case 'nano':
|
|
129
|
+
return 'nano-claude-code is not on PATH. Install from https://github.com/OpenLAIR/nano-claude-code or set NANO_CLAUDE_CODE_COMMAND to the executable.';
|
|
91
130
|
default:
|
|
92
131
|
return 'Required CLI is not installed. Install it first, then retry login.';
|
|
93
132
|
}
|
|
@@ -217,6 +256,30 @@ router.get('/providers', async (_req, res) => {
|
|
|
217
256
|
}
|
|
218
257
|
});
|
|
219
258
|
|
|
259
|
+
async function checkNanoCliForInstall() {
|
|
260
|
+
const resolvedCliCommand = await resolveAvailableCliCommand({
|
|
261
|
+
envVarName: 'NANO_CLAUDE_CODE_COMMAND',
|
|
262
|
+
defaultCommands: ['nano-claude-code'],
|
|
263
|
+
args: ['--version'],
|
|
264
|
+
appendWindowsSuffixes: true,
|
|
265
|
+
});
|
|
266
|
+
if (!resolvedCliCommand) {
|
|
267
|
+
return {
|
|
268
|
+
authenticated: false,
|
|
269
|
+
email: null,
|
|
270
|
+
error: 'nano-claude-code CLI not found',
|
|
271
|
+
cliAvailable: false,
|
|
272
|
+
cliCommand: 'nano-claude-code',
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
return {
|
|
276
|
+
authenticated: true,
|
|
277
|
+
email: 'Nano Claude Code',
|
|
278
|
+
cliAvailable: true,
|
|
279
|
+
cliCommand: resolvedCliCommand,
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
|
|
220
283
|
router.post('/install/:provider', async (req, res) => {
|
|
221
284
|
const provider = String(req.params.provider || '').toLowerCase();
|
|
222
285
|
const definition = getInstallerDefinition(provider);
|
|
@@ -230,7 +293,7 @@ router.post('/install/:provider', async (req, res) => {
|
|
|
230
293
|
return res.status(400).json({
|
|
231
294
|
ok: false,
|
|
232
295
|
provider,
|
|
233
|
-
error: `No automatic installer is configured for ${definition.displayName} on ${process.platform}
|
|
296
|
+
error: `No automatic installer is configured for ${definition.displayName} on ${process.platform}. See the official docs for manual Linux steps.`,
|
|
234
297
|
docsUrl: definition.docsUrl,
|
|
235
298
|
downloadUrl: definition.fallbackDownloadUrl,
|
|
236
299
|
});
|
|
@@ -245,7 +308,9 @@ router.post('/install/:provider', async (req, res) => {
|
|
|
245
308
|
? checkCursorStatus
|
|
246
309
|
: provider === 'codex'
|
|
247
310
|
? checkCodexCredentials
|
|
248
|
-
:
|
|
311
|
+
: provider === 'nano'
|
|
312
|
+
? checkNanoCliForInstall
|
|
313
|
+
: checkGeminiCredentials;
|
|
249
314
|
|
|
250
315
|
const latestStatus = await checker();
|
|
251
316
|
|
|
@@ -371,6 +436,42 @@ router.get('/gemini/status', async (req, res) => {
|
|
|
371
436
|
}
|
|
372
437
|
});
|
|
373
438
|
|
|
439
|
+
router.get('/nano/status', async (req, res) => {
|
|
440
|
+
try {
|
|
441
|
+
const resolvedCliCommand = await resolveAvailableCliCommand({
|
|
442
|
+
envVarName: 'NANO_CLAUDE_CODE_COMMAND',
|
|
443
|
+
defaultCommands: ['nano-claude-code'],
|
|
444
|
+
args: ['--version'],
|
|
445
|
+
appendWindowsSuffixes: true,
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
if (!resolvedCliCommand) {
|
|
449
|
+
return res.json(buildStatusPayload({
|
|
450
|
+
authenticated: false,
|
|
451
|
+
email: null,
|
|
452
|
+
error: 'nano-claude-code CLI not found',
|
|
453
|
+
cliAvailable: false,
|
|
454
|
+
cliCommand: 'nano-claude-code',
|
|
455
|
+
installHint: buildCliInstallHint('nano'),
|
|
456
|
+
}, 'nano'));
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
return res.json(buildStatusPayload({
|
|
460
|
+
authenticated: true,
|
|
461
|
+
email: 'Nano Claude Code',
|
|
462
|
+
cliAvailable: true,
|
|
463
|
+
cliCommand: resolvedCliCommand,
|
|
464
|
+
}, 'nano'));
|
|
465
|
+
} catch (error) {
|
|
466
|
+
console.error('Error checking Nano Claude Code status:', error);
|
|
467
|
+
res.status(500).json({
|
|
468
|
+
authenticated: false,
|
|
469
|
+
email: null,
|
|
470
|
+
error: error.message,
|
|
471
|
+
});
|
|
472
|
+
}
|
|
473
|
+
});
|
|
474
|
+
|
|
374
475
|
async function checkGeminiCredentials() {
|
|
375
476
|
console.log('[DEBUG] Checking Gemini credentials...');
|
|
376
477
|
let cliCommand = process.env.GEMINI_CLI_PATH || 'gemini';
|
|
@@ -126,6 +126,12 @@ const builtInCommands = [
|
|
|
126
126
|
description: 'Rewind the conversation to a previous state',
|
|
127
127
|
namespace: 'builtin',
|
|
128
128
|
metadata: { type: 'builtin' }
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
name: '/btw',
|
|
132
|
+
description: 'Ask a quick side question in an overlay (Claude only; does not add to chat history)',
|
|
133
|
+
namespace: 'builtin',
|
|
134
|
+
metadata: { type: 'builtin' }
|
|
129
135
|
}
|
|
130
136
|
];
|
|
131
137
|
|
|
@@ -396,7 +402,25 @@ Custom commands can be created in:
|
|
|
396
402
|
message: `Rewinding conversation by ${steps} step${steps > 1 ? 's' : ''}...`
|
|
397
403
|
}
|
|
398
404
|
};
|
|
399
|
-
}
|
|
405
|
+
},
|
|
406
|
+
|
|
407
|
+
'/btw': async (args) => {
|
|
408
|
+
const question = args.join(' ').trim();
|
|
409
|
+
if (!question) {
|
|
410
|
+
return {
|
|
411
|
+
type: 'builtin',
|
|
412
|
+
action: 'btw',
|
|
413
|
+
data: {
|
|
414
|
+
error: 'Usage: /btw <your question>',
|
|
415
|
+
},
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
return {
|
|
419
|
+
type: 'builtin',
|
|
420
|
+
action: 'btw',
|
|
421
|
+
data: { question },
|
|
422
|
+
};
|
|
423
|
+
},
|
|
400
424
|
};
|
|
401
425
|
|
|
402
426
|
/**
|
|
@@ -121,27 +121,32 @@ router.post('/configure', async (req, res) => {
|
|
|
121
121
|
// ── Step 2: Register MCP server ──
|
|
122
122
|
if (mcpBackend) {
|
|
123
123
|
try {
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
const mcpCommands = {
|
|
137
|
-
codex: ['mcp', 'add', 'codex', '-s', 'user', '--', 'codex', 'mcp-server'],
|
|
138
|
-
'llm-chat': ['mcp', 'add', 'llm-chat', '-s', 'user', '--', 'python3', path.join(resolvedPath, 'skills/aris-infra/mcp-servers/llm-chat/server.py')],
|
|
139
|
-
gemini: ['mcp', 'add', 'gemini-review', '-s', 'user', '--', 'python3', path.join(resolvedPath, 'skills/aris-infra/mcp-servers/gemini-review/server.py')],
|
|
140
|
-
};
|
|
141
|
-
|
|
142
|
-
const args = mcpCommands[mcpBackend];
|
|
143
|
-
if (!args) {
|
|
124
|
+
const mcpCommands = {
|
|
125
|
+
codex: ['mcp', 'add', 'codex', '-s', 'user', '--', 'codex', 'mcp-server'],
|
|
126
|
+
'llm-chat': ['mcp', 'add', 'llm-chat', '-s', 'user', '--', 'python3', path.join(resolvedPath, 'skills/aris-infra/mcp-servers/llm-chat/server.py')],
|
|
127
|
+
gemini: ['mcp', 'add', 'gemini-review', '-s', 'user', '--', 'python3', path.join(resolvedPath, 'skills/aris-infra/mcp-servers/gemini-review/server.py')],
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
const ENV_ONLY_BACKENDS = ['gemini-figure'];
|
|
131
|
+
const args = mcpCommands[mcpBackend];
|
|
132
|
+
if (!args) {
|
|
133
|
+
if (ENV_ONLY_BACKENDS.includes(mcpBackend)) {
|
|
134
|
+
results.steps.push({ step: 'mcp', status: 'skipped', message: `No MCP server needed for ${mcpBackend}` });
|
|
135
|
+
} else {
|
|
144
136
|
results.errors.push({ step: 'mcp', error: `Unknown backend: ${mcpBackend}` });
|
|
137
|
+
}
|
|
138
|
+
} else {
|
|
139
|
+
// Check if already registered
|
|
140
|
+
let alreadyRegistered = false;
|
|
141
|
+
try {
|
|
142
|
+
const { stdout } = await spawnAsync('claude', ['mcp', 'list'], { env: EXTENDED_ENV });
|
|
143
|
+
alreadyRegistered = stdout.includes(mcpBackend);
|
|
144
|
+
} catch {
|
|
145
|
+
// claude mcp list failed, proceed with registration
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (alreadyRegistered) {
|
|
149
|
+
results.steps.push({ step: 'mcp', status: 'skipped', message: `${mcpBackend} MCP already registered` });
|
|
145
150
|
} else {
|
|
146
151
|
await spawnAsync('claude', args, { env: EXTENDED_ENV });
|
|
147
152
|
results.steps.push({ step: 'mcp', status: 'ok', message: `Registered ${mcpBackend} MCP server` });
|
|
@@ -200,7 +205,7 @@ router.post('/configure', async (req, res) => {
|
|
|
200
205
|
let linked = 0;
|
|
201
206
|
const entries = await fs.readdir(skillsDir, { withFileTypes: true });
|
|
202
207
|
for (const entry of entries) {
|
|
203
|
-
if (entry.isDirectory() && (entry.name.startsWith('aris-') || entry.name === 'autoresearch')) {
|
|
208
|
+
if (entry.isDirectory() && (entry.name.startsWith('aris-') || entry.name.startsWith('inno-') || entry.name === 'autoresearch')) {
|
|
204
209
|
const target = path.join(claudeSkillsDir, entry.name);
|
|
205
210
|
try {
|
|
206
211
|
await fs.access(target);
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import express from 'express';
|
|
2
|
+
import { memoryDb } from '../database/db.js';
|
|
3
|
+
import { syncMemoryFiles } from '../utils/memoryPrompt.js';
|
|
4
|
+
|
|
5
|
+
const router = express.Router();
|
|
6
|
+
const MAX_CONTENT_LENGTH = 500;
|
|
7
|
+
const VALID_CATEGORIES = new Set(['general', 'preference', 'context', 'workflow']);
|
|
8
|
+
|
|
9
|
+
/** Parse and validate :id param as a positive integer. */
|
|
10
|
+
function parseId(raw) {
|
|
11
|
+
const id = parseInt(raw, 10);
|
|
12
|
+
return Number.isNaN(id) || id <= 0 ? null : id;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// ===============================
|
|
16
|
+
// Memory CRUD
|
|
17
|
+
// ===============================
|
|
18
|
+
|
|
19
|
+
// Get all memories for the authenticated user
|
|
20
|
+
router.get('/', async (req, res) => {
|
|
21
|
+
try {
|
|
22
|
+
const memories = memoryDb.getAll(req.user.id);
|
|
23
|
+
res.json({ memories });
|
|
24
|
+
} catch (error) {
|
|
25
|
+
console.error('Error fetching memories:', error);
|
|
26
|
+
res.status(500).json({ error: 'Failed to fetch memories' });
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
// Create a new memory
|
|
31
|
+
router.post('/', async (req, res) => {
|
|
32
|
+
try {
|
|
33
|
+
const { content, category } = req.body;
|
|
34
|
+
if (!content || !content.trim()) {
|
|
35
|
+
return res.status(400).json({ error: 'Memory content is required' });
|
|
36
|
+
}
|
|
37
|
+
if (content.length > MAX_CONTENT_LENGTH) {
|
|
38
|
+
return res.status(400).json({ error: `Memory content must be ${MAX_CONTENT_LENGTH} characters or less` });
|
|
39
|
+
}
|
|
40
|
+
const cat = VALID_CATEGORIES.has(category) ? category : 'general';
|
|
41
|
+
const memory = memoryDb.create(req.user.id, content.trim(), cat);
|
|
42
|
+
syncMemoryFiles(req.user.id).catch(err => console.warn('[memory] sync error:', err.message));
|
|
43
|
+
res.status(201).json({ memory });
|
|
44
|
+
} catch (error) {
|
|
45
|
+
if (error.message?.includes('Maximum of')) {
|
|
46
|
+
return res.status(400).json({ error: error.message });
|
|
47
|
+
}
|
|
48
|
+
console.error('Error creating memory:', error);
|
|
49
|
+
res.status(500).json({ error: 'Failed to create memory' });
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// Update a memory
|
|
54
|
+
router.put('/:id', async (req, res) => {
|
|
55
|
+
try {
|
|
56
|
+
const id = parseId(req.params.id);
|
|
57
|
+
if (!id) return res.status(400).json({ error: 'Invalid memory ID' });
|
|
58
|
+
|
|
59
|
+
const { content, category } = req.body;
|
|
60
|
+
if (content !== undefined && content.length > MAX_CONTENT_LENGTH) {
|
|
61
|
+
return res.status(400).json({ error: `Memory content must be ${MAX_CONTENT_LENGTH} characters or less` });
|
|
62
|
+
}
|
|
63
|
+
const updates = {};
|
|
64
|
+
if (content !== undefined) updates.content = content.trim();
|
|
65
|
+
if (category !== undefined) updates.category = VALID_CATEGORIES.has(category) ? category : 'general';
|
|
66
|
+
|
|
67
|
+
const memory = memoryDb.update(req.user.id, id, updates);
|
|
68
|
+
if (!memory) {
|
|
69
|
+
return res.status(404).json({ error: 'Memory not found' });
|
|
70
|
+
}
|
|
71
|
+
syncMemoryFiles(req.user.id).catch(err => console.warn('[memory] sync error:', err.message));
|
|
72
|
+
res.json({ memory });
|
|
73
|
+
} catch (error) {
|
|
74
|
+
console.error('Error updating memory:', error);
|
|
75
|
+
res.status(500).json({ error: 'Failed to update memory' });
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
// Toggle memory enabled/disabled
|
|
80
|
+
router.patch('/:id/toggle', async (req, res) => {
|
|
81
|
+
try {
|
|
82
|
+
const id = parseId(req.params.id);
|
|
83
|
+
if (!id) return res.status(400).json({ error: 'Invalid memory ID' });
|
|
84
|
+
|
|
85
|
+
const memory = memoryDb.toggle(req.user.id, id);
|
|
86
|
+
if (!memory) {
|
|
87
|
+
return res.status(404).json({ error: 'Memory not found' });
|
|
88
|
+
}
|
|
89
|
+
syncMemoryFiles(req.user.id).catch(err => console.warn('[memory] sync error:', err.message));
|
|
90
|
+
res.json({ memory });
|
|
91
|
+
} catch (error) {
|
|
92
|
+
console.error('Error toggling memory:', error);
|
|
93
|
+
res.status(500).json({ error: 'Failed to toggle memory' });
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// Delete a memory
|
|
98
|
+
router.delete('/:id', async (req, res) => {
|
|
99
|
+
try {
|
|
100
|
+
const id = parseId(req.params.id);
|
|
101
|
+
if (!id) return res.status(400).json({ error: 'Invalid memory ID' });
|
|
102
|
+
|
|
103
|
+
const deleted = memoryDb.delete(req.user.id, id);
|
|
104
|
+
if (!deleted) {
|
|
105
|
+
return res.status(404).json({ error: 'Memory not found' });
|
|
106
|
+
}
|
|
107
|
+
syncMemoryFiles(req.user.id).catch(err => console.warn('[memory] sync error:', err.message));
|
|
108
|
+
res.json({ success: true });
|
|
109
|
+
} catch (error) {
|
|
110
|
+
console.error('Error deleting memory:', error);
|
|
111
|
+
res.status(500).json({ error: 'Failed to delete memory' });
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
// ===============================
|
|
116
|
+
// Per-User Memory Settings
|
|
117
|
+
// ===============================
|
|
118
|
+
|
|
119
|
+
// Get memory enabled state for the authenticated user
|
|
120
|
+
router.get('/settings', async (req, res) => {
|
|
121
|
+
try {
|
|
122
|
+
const enabled = memoryDb.getMemoryEnabled(req.user.id);
|
|
123
|
+
res.json({ enabled });
|
|
124
|
+
} catch (error) {
|
|
125
|
+
console.error('Error fetching memory settings:', error);
|
|
126
|
+
res.status(500).json({ error: 'Failed to fetch memory settings' });
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
// Set memory enabled state for the authenticated user
|
|
131
|
+
router.patch('/settings', async (req, res) => {
|
|
132
|
+
try {
|
|
133
|
+
const { enabled } = req.body;
|
|
134
|
+
memoryDb.setMemoryEnabled(req.user.id, !!enabled);
|
|
135
|
+
syncMemoryFiles(req.user.id).catch(err => console.warn('[memory] sync error:', err.message));
|
|
136
|
+
res.json({ enabled: !!enabled });
|
|
137
|
+
} catch (error) {
|
|
138
|
+
console.error('Error updating memory settings:', error);
|
|
139
|
+
res.status(500).json({ error: 'Failed to update memory settings' });
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
export default router;
|
package/server/routes/news.js
CHANGED
|
@@ -488,6 +488,9 @@ router.post('/search/:source', (req, res) => handleSearch(req.params.source, req
|
|
|
488
488
|
// GET /api/news/logs/:source — poll search progress logs
|
|
489
489
|
router.get('/logs/:source', async (req, res) => {
|
|
490
490
|
try {
|
|
491
|
+
const entry = getSourceEntry(req.params.source);
|
|
492
|
+
if (!entry) return res.status(404).json({ error: `Unknown source: ${req.params.source}` });
|
|
493
|
+
|
|
491
494
|
const logPath = path.join(DATA_DIR, `news-log-${req.params.source}.json`);
|
|
492
495
|
const data = await fs.readFile(logPath, 'utf8');
|
|
493
496
|
res.json({ logs: JSON.parse(data) });
|
package/server/routes/skills.js
CHANGED
|
@@ -5,6 +5,7 @@ import os from 'os';
|
|
|
5
5
|
import { fileURLToPath } from 'url';
|
|
6
6
|
import { extractProjectDirectory, ensureProjectSkillLinks } from '../projects.js';
|
|
7
7
|
import { FORBIDDEN_PATHS } from './projects.js';
|
|
8
|
+
import { findSkillMdPath } from '../utils/skillExpander.js';
|
|
8
9
|
|
|
9
10
|
const GLOBAL_SKILLS_DIR = path.join(path.dirname(fileURLToPath(import.meta.url)), '..', '..', 'skills');
|
|
10
11
|
|
|
@@ -223,6 +224,40 @@ async function validateZipBuffer(buffer) {
|
|
|
223
224
|
};
|
|
224
225
|
}
|
|
225
226
|
|
|
227
|
+
// GET /resolve — resolve a skill name to its SKILL.md path and content
|
|
228
|
+
router.get('/resolve', async (req, res) => {
|
|
229
|
+
try {
|
|
230
|
+
const { name, workingDir } = req.query;
|
|
231
|
+
if (!name || typeof name !== 'string') {
|
|
232
|
+
return res.status(400).json({ error: 'name query parameter is required' });
|
|
233
|
+
}
|
|
234
|
+
// Prevent path traversal
|
|
235
|
+
if (/[/\\]|\.\./.test(name)) {
|
|
236
|
+
return res.status(400).json({ error: 'Invalid skill name' });
|
|
237
|
+
}
|
|
238
|
+
const effectiveWorkingDir = (typeof workingDir === 'string' && workingDir.trim()) || process.cwd();
|
|
239
|
+
// Validate workingDir: must be absolute and not a forbidden system path
|
|
240
|
+
if (effectiveWorkingDir !== process.cwd()) {
|
|
241
|
+
if (!path.isAbsolute(effectiveWorkingDir)) {
|
|
242
|
+
return res.status(400).json({ error: 'workingDir must be an absolute path' });
|
|
243
|
+
}
|
|
244
|
+
const normalized = path.normalize(effectiveWorkingDir);
|
|
245
|
+
if (FORBIDDEN_PATHS.some((fp) => normalized === fp || normalized.startsWith(fp + path.sep))) {
|
|
246
|
+
return res.status(400).json({ error: 'Invalid workingDir' });
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
const skillMdPath = await findSkillMdPath(name, effectiveWorkingDir);
|
|
250
|
+
if (!skillMdPath) {
|
|
251
|
+
return res.status(404).json({ error: 'Skill not found' });
|
|
252
|
+
}
|
|
253
|
+
const content = await fs.readFile(skillMdPath, 'utf-8');
|
|
254
|
+
res.json({ path: skillMdPath, content });
|
|
255
|
+
} catch (error) {
|
|
256
|
+
console.error('[skills] resolve error:', error.message);
|
|
257
|
+
res.status(500).json({ error: error.message });
|
|
258
|
+
}
|
|
259
|
+
});
|
|
260
|
+
|
|
226
261
|
// GET / — return the file tree of the global skills/ directory
|
|
227
262
|
router.get('/', async (req, res) => {
|
|
228
263
|
try {
|
package/server/routes/user.js
CHANGED
|
@@ -2,10 +2,8 @@ import express from 'express';
|
|
|
2
2
|
import { userDb } from '../database/db.js';
|
|
3
3
|
import { authenticateToken } from '../middleware/auth.js';
|
|
4
4
|
import { getSystemGitConfig } from '../utils/gitConfig.js';
|
|
5
|
-
import
|
|
6
|
-
import { promisify } from 'util';
|
|
5
|
+
import spawnAsync from '../utils/spawnAsync.js';
|
|
7
6
|
|
|
8
|
-
const execAsync = promisify(exec);
|
|
9
7
|
const router = express.Router();
|
|
10
8
|
|
|
11
9
|
router.get('/git-config', authenticateToken, async (req, res) => {
|
|
@@ -55,8 +53,8 @@ router.post('/git-config', authenticateToken, async (req, res) => {
|
|
|
55
53
|
userDb.updateGitConfig(userId, gitName, gitEmail);
|
|
56
54
|
|
|
57
55
|
try {
|
|
58
|
-
await
|
|
59
|
-
await
|
|
56
|
+
await spawnAsync('git', ['config', '--global', 'user.name', gitName]);
|
|
57
|
+
await spawnAsync('git', ['config', '--global', 'user.email', gitEmail]);
|
|
60
58
|
console.log(`Applied git config globally: ${gitName} <${gitEmail}>`);
|
|
61
59
|
} catch (gitError) {
|
|
62
60
|
console.error('Error applying git config:', gitError);
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
import os from 'os';
|
|
5
|
+
import { safePath } from '../safePath.js';
|
|
6
|
+
|
|
7
|
+
// Use a real temporary directory so realpathSync works correctly
|
|
8
|
+
let ROOT;
|
|
9
|
+
|
|
10
|
+
beforeAll(() => {
|
|
11
|
+
// Use realpathSync to normalize macOS /var -> /private/var symlink
|
|
12
|
+
ROOT = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'safepath-test-')));
|
|
13
|
+
fs.mkdirSync(path.join(ROOT, 'src'), { recursive: true });
|
|
14
|
+
fs.mkdirSync(path.join(ROOT, 'lib'), { recursive: true });
|
|
15
|
+
fs.writeFileSync(path.join(ROOT, 'src', 'index.js'), '');
|
|
16
|
+
fs.writeFileSync(path.join(ROOT, 'lib', 'utils.js'), '');
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
afterAll(() => {
|
|
20
|
+
fs.rmSync(ROOT, { recursive: true, force: true });
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
describe('safePath', () => {
|
|
24
|
+
it('resolves relative paths within root', () => {
|
|
25
|
+
const result = safePath('src/index.js', ROOT);
|
|
26
|
+
expect(result).toBe(path.join(ROOT, 'src', 'index.js'));
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('returns root when path is empty/null/undefined', () => {
|
|
30
|
+
expect(safePath('', ROOT)).toBe(ROOT);
|
|
31
|
+
expect(safePath(null, ROOT)).toBe(ROOT);
|
|
32
|
+
expect(safePath(undefined, ROOT)).toBe(ROOT);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('allows absolute paths that land inside root', () => {
|
|
36
|
+
const absInside = path.join(ROOT, 'src', 'index.js');
|
|
37
|
+
const result = safePath(absInside, ROOT);
|
|
38
|
+
expect(result).toBe(absInside);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('blocks absolute paths outside root', () => {
|
|
42
|
+
expect(() => safePath('/etc/passwd', ROOT)).toThrow(/Path traversal blocked/);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('blocks .. traversal above root', () => {
|
|
46
|
+
expect(() => safePath('../../../etc/shadow', ROOT)).toThrow(/Path traversal blocked/);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('blocks .. traversal disguised in deeper path', () => {
|
|
50
|
+
expect(() => safePath('src/../../../../../../etc/passwd', ROOT)).toThrow(
|
|
51
|
+
/Path traversal blocked/,
|
|
52
|
+
);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('allows .. that stays within root', () => {
|
|
56
|
+
const result = safePath('src/../lib/utils.js', ROOT);
|
|
57
|
+
expect(result).toBe(path.join(ROOT, 'lib', 'utils.js'));
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('handles non-existent target gracefully', () => {
|
|
61
|
+
// Non-existent file in existing directory — should work
|
|
62
|
+
const result = safePath('src/newfile.js', ROOT);
|
|
63
|
+
expect(result).toBe(path.join(ROOT, 'src', 'newfile.js'));
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('handles non-existent nested path gracefully', () => {
|
|
67
|
+
// Non-existent nested path — should still resolve within root
|
|
68
|
+
const result = safePath('deep/nested/new/file.js', ROOT);
|
|
69
|
+
expect(result.startsWith(ROOT + path.sep)).toBe(true);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('normalizes allowedRoot for falsy input', () => {
|
|
73
|
+
// Pass a non-normalized root (with trailing segments) to verify
|
|
74
|
+
// the falsy-input path returns path.resolve(allowedRoot), not the raw string.
|
|
75
|
+
const nonNormalized = ROOT + path.sep + 'src' + path.sep + '..';
|
|
76
|
+
const result = safePath('', nonNormalized);
|
|
77
|
+
expect(result).toBe(ROOT);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('allows symlinks inside the project that point outside the root', () => {
|
|
81
|
+
// Simulate: project/data -> /tmp (an external location)
|
|
82
|
+
// This should NOT be blocked — legitimate workflow (shared datasets, etc.)
|
|
83
|
+
const linkPath = path.join(ROOT, 'external-data');
|
|
84
|
+
try {
|
|
85
|
+
fs.symlinkSync(os.tmpdir(), linkPath);
|
|
86
|
+
// Logical path is inside root, so safePath should allow it
|
|
87
|
+
const result = safePath('external-data/some-file.csv', ROOT);
|
|
88
|
+
expect(result).toBe(path.join(ROOT, 'external-data', 'some-file.csv'));
|
|
89
|
+
} finally {
|
|
90
|
+
try { fs.unlinkSync(linkPath); } catch { /* ignore cleanup errors */ }
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
});
|
|
@@ -9,6 +9,7 @@ function isCommandNotFoundExitCode(code) {
|
|
|
9
9
|
*
|
|
10
10
|
* @param {Object} options
|
|
11
11
|
* @param {string} options.envVarName Environment variable name containing an override command.
|
|
12
|
+
* @param {string[]} [options.legacyEnvVarNames=[]] Older env var names checked after envVarName (migration).
|
|
12
13
|
* @param {string[]} options.defaultCommands Fallback command names in preference order.
|
|
13
14
|
* @param {string} [options.platform=process.platform] Runtime platform, used for Windows suffix handling.
|
|
14
15
|
* @param {boolean} [options.appendWindowsSuffixes=false] Whether to append .cmd/.exe candidates on Windows.
|
|
@@ -16,11 +17,19 @@ function isCommandNotFoundExitCode(code) {
|
|
|
16
17
|
*/
|
|
17
18
|
function getCliCommandCandidates({
|
|
18
19
|
envVarName,
|
|
20
|
+
legacyEnvVarNames = [],
|
|
19
21
|
defaultCommands,
|
|
20
22
|
platform = process.platform,
|
|
21
23
|
appendWindowsSuffixes = false
|
|
22
24
|
}) {
|
|
23
|
-
|
|
25
|
+
let envCommand = '';
|
|
26
|
+
for (const key of [envVarName, ...legacyEnvVarNames].filter(Boolean)) {
|
|
27
|
+
const s = String(process.env[key] || '').trim();
|
|
28
|
+
if (s) {
|
|
29
|
+
envCommand = s;
|
|
30
|
+
break;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
24
33
|
const rawCandidates = [];
|
|
25
34
|
|
|
26
35
|
if (envCommand) {
|
|
@@ -131,6 +140,7 @@ function checkCommandAvailable(command, args = ['--help'], { platform = process.
|
|
|
131
140
|
*
|
|
132
141
|
* @param {Object} options
|
|
133
142
|
* @param {string} options.envVarName Environment variable with command override.
|
|
143
|
+
* @param {string[]} [options.legacyEnvVarNames=[]] Older env vars (after envVarName) for migration.
|
|
134
144
|
* @param {string[]} options.defaultCommands Fallback command names in preference order.
|
|
135
145
|
* @param {string[]} [options.args=['--help']] Probe arguments.
|
|
136
146
|
* @param {string} [options.platform=process.platform] Runtime platform.
|
|
@@ -140,6 +150,7 @@ function checkCommandAvailable(command, args = ['--help'], { platform = process.
|
|
|
140
150
|
*/
|
|
141
151
|
async function resolveAvailableCliCommand({
|
|
142
152
|
envVarName,
|
|
153
|
+
legacyEnvVarNames = [],
|
|
143
154
|
defaultCommands,
|
|
144
155
|
args = ['--help'],
|
|
145
156
|
platform = process.platform,
|
|
@@ -148,6 +159,7 @@ async function resolveAvailableCliCommand({
|
|
|
148
159
|
}) {
|
|
149
160
|
const candidates = getCliCommandCandidates({
|
|
150
161
|
envVarName,
|
|
162
|
+
legacyEnvVarNames,
|
|
151
163
|
defaultCommands,
|
|
152
164
|
platform,
|
|
153
165
|
appendWindowsSuffixes
|