dr-claw 1.1.2 → 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/README.md +286 -177
- package/README.zh-CN.md +286 -174
- package/dist/assets/{_basePickBy-C0n1uo4u.js → _basePickBy-7CEpiVqc.js} +1 -1
- package/dist/assets/{_baseUniq-BRNmhFkc.js → _baseUniq-yh8GoKah.js} +1 -1
- package/dist/assets/{arc-DOMBmC_A.js → arc-xxYot-So.js} +1 -1
- package/dist/assets/{architectureDiagram-VXUJARFQ-CtMWglbD.js → architectureDiagram-VXUJARFQ-Bb1NqOFB.js} +1 -1
- package/dist/assets/{blockDiagram-VD42YOAC-DK_gTkJy.js → blockDiagram-VD42YOAC-D8xMHk8C.js} +1 -1
- package/dist/assets/{c4Diagram-YG6GDRKO-DrDAvsn7.js → c4Diagram-YG6GDRKO-69UiBMVa.js} +1 -1
- package/dist/assets/channel-BJdHHkhR.js +1 -0
- package/dist/assets/{chunk-4BX2VUAB-CGbhdZcN.js → chunk-4BX2VUAB-DXrPhE6K.js} +1 -1
- package/dist/assets/{chunk-55IACEB6-sGeFSxen.js → chunk-55IACEB6-B3sw4AGX.js} +1 -1
- package/dist/assets/{chunk-B4BG7PRW-RxgHAU_P.js → chunk-B4BG7PRW-Dd4n17kR.js} +1 -1
- package/dist/assets/{chunk-DI55MBZ5-CvHUXZrx.js → chunk-DI55MBZ5-DuTb9gb8.js} +1 -1
- package/dist/assets/{chunk-FMBD7UC4-Sn7cXVNU.js → chunk-FMBD7UC4-CGErQ-ud.js} +1 -1
- package/dist/assets/{chunk-QN33PNHL-bDKOHppN.js → chunk-QN33PNHL-Bk5fuYbR.js} +1 -1
- package/dist/assets/{chunk-QZHKN3VN-B9kN5LGm.js → chunk-QZHKN3VN-2UdZTbge.js} +1 -1
- package/dist/assets/{chunk-TZMSLE5B-CCBe6len.js → chunk-TZMSLE5B-Bl-UYKEK.js} +1 -1
- package/dist/assets/{classDiagram-2ON5EDUG-DQoMNYlY.js → classDiagram-2ON5EDUG-EaoBDWqJ.js} +1 -1
- package/dist/assets/{classDiagram-v2-WZHVMYZB-DQoMNYlY.js → classDiagram-v2-WZHVMYZB-EaoBDWqJ.js} +1 -1
- package/dist/assets/clone-CwXTbwxl.js +1 -0
- package/dist/assets/{cose-bilkent-S5V4N54A-r6sLiHBW.js → cose-bilkent-S5V4N54A-qXNKLX2R.js} +1 -1
- package/dist/assets/{dagre-6UL2VRFP-AbsaR1Oa.js → dagre-6UL2VRFP-DmHvDWep.js} +1 -1
- package/dist/assets/{diagram-PSM6KHXK-DjCvdUyy.js → diagram-PSM6KHXK-C_Rj4DX3.js} +1 -1
- package/dist/assets/{diagram-QEK2KX5R-CqiocXkH.js → diagram-QEK2KX5R-BVr3mPXW.js} +1 -1
- package/dist/assets/{diagram-S2PKOQOG-D5ALeoII.js → diagram-S2PKOQOG-DVx6i0Qe.js} +1 -1
- package/dist/assets/{erDiagram-Q2GNP2WA-C1AeOsiz.js → erDiagram-Q2GNP2WA-BJQyv9_t.js} +1 -1
- package/dist/assets/{flowDiagram-NV44I4VS-C98ve_DO.js → flowDiagram-NV44I4VS-DCR8sPAE.js} +1 -1
- package/dist/assets/{ganttDiagram-JELNMOA3-3zdLvcxD.js → ganttDiagram-JELNMOA3-BcXFAthK.js} +1 -1
- package/dist/assets/{gitGraphDiagram-V2S2FVAM-nDuDMsHh.js → gitGraphDiagram-V2S2FVAM-CnS56w1x.js} +1 -1
- package/dist/assets/{graph-Blm-emkR.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-CmzE3q0R.js → infoDiagram-HS3SLOUP-DtI272ZD.js} +1 -1
- package/dist/assets/{journeyDiagram-XKPGCS4Q-CYaW7LrP.js → journeyDiagram-XKPGCS4Q-DHp_Kica.js} +1 -1
- package/dist/assets/{kanban-definition-3W4ZIXB7-mkmW7p0j.js → kanban-definition-3W4ZIXB7-CjgHMNku.js} +1 -1
- package/dist/assets/{layout-Ct2kFIId.js → layout-Cnc-DCSb.js} +1 -1
- package/dist/assets/{linear-CKG9EwIJ.js → linear-BtRgmV19.js} +1 -1
- package/dist/assets/{mermaid.core-CaKcUnnd.js → mermaid.core-B20uzv6N.js} +5 -5
- package/dist/assets/{mindmap-definition-VGOIOE7T-Bct-pYin.js → mindmap-definition-VGOIOE7T-bbcLXkWp.js} +1 -1
- package/dist/assets/{pieDiagram-ADFJNKIX-C8wFD4Jr.js → pieDiagram-ADFJNKIX-B4fEzWKO.js} +1 -1
- package/dist/assets/{quadrantDiagram-AYHSOK5B-CuiE_f46.js → quadrantDiagram-AYHSOK5B-B5G0SMbK.js} +1 -1
- package/dist/assets/{requirementDiagram-UZGBJVZJ-C4v1CqU8.js → requirementDiagram-UZGBJVZJ-DsqtOFgj.js} +1 -1
- package/dist/assets/{sankeyDiagram-TZEHDZUN-keAqqs-Y.js → sankeyDiagram-TZEHDZUN-CiGemFRE.js} +1 -1
- package/dist/assets/{sequenceDiagram-WL72ISMW-ClNbvKTB.js → sequenceDiagram-WL72ISMW-hEMvYbe9.js} +1 -1
- package/dist/assets/{stateDiagram-FKZM4ZOC-D8We-2vq.js → stateDiagram-FKZM4ZOC-Cn8VB7b1.js} +1 -1
- package/dist/assets/stateDiagram-v2-4FDKWEC3-DaH0PZOq.js +1 -0
- package/dist/assets/{timeline-definition-IT6M3QCI-DsvEMT5T.js → timeline-definition-IT6M3QCI-DChT1GUr.js} +1 -1
- package/dist/assets/{treemap-GDKQZRPO-TJrzN7Bj.js → treemap-GDKQZRPO-Dflv6a8z.js} +1 -1
- package/dist/assets/{xychartDiagram-PRI3JC2R-BdATmY5F.js → xychartDiagram-PRI3JC2R-Dr4urXR8.js} +1 -1
- package/dist/icons/nanoclaude.png +0 -0
- package/dist/index.html +2 -2
- package/dist/screenshots/screenshot_v1_2.png +0 -0
- package/package.json +61 -4
- package/public/icons/nanoclaude.png +0 -0
- package/public/screenshots/screenshot_v1_2.png +0 -0
- package/scripts/build-electron-icons.mjs +40 -0
- package/scripts/check-deps.js +28 -0
- package/scripts/native-runtime.mjs +128 -0
- package/server/__tests__/gemini-api.test.mjs +699 -0
- package/server/__tests__/gemini-session-index.test.mjs +95 -0
- package/server/__tests__/project-sync-dedup.test.mjs +173 -0
- package/server/__tests__/session-delete.test.mjs +152 -0
- package/server/claude-sdk.js +175 -14
- package/server/cli-chat.js +28 -17
- package/server/cli.js +48 -11
- 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 +101 -5
- 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 -16
- package/server/load-env.js +13 -1
- package/server/local-gpu.js +166 -24
- package/server/mcp-compute.js +223 -0
- package/server/middleware/auth.js +25 -6
- 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 +516 -110
- 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 +422 -58
- package/server/routes/commands.js +25 -1
- package/server/routes/community-tools.js +398 -0
- package/server/routes/compute.js +155 -0
- package/server/routes/git.js +9 -11
- 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/codexSessionMessages.js +285 -0
- 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-BFOE7OyC.js +0 -1
- package/dist/assets/clone-BulQSblH.js +0 -1
- package/dist/assets/index-B8Yutsnm.css +0 -1
- package/dist/assets/index-Cf6gyhc3.js +0 -654
- package/dist/assets/stateDiagram-v2-4FDKWEC3-BUURfjrE.js +0 -1
- package/scripts/install-python-tools.js +0 -64
|
@@ -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
|
/**
|
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
import express from 'express';
|
|
2
|
+
import { spawn } from 'child_process';
|
|
3
|
+
import { promises as fs } from 'fs';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import os from 'os';
|
|
6
|
+
import { loadAllNodes, loadNodeConfig, ComputeNode } from '../compute-node.js';
|
|
7
|
+
|
|
8
|
+
const router = express.Router();
|
|
9
|
+
|
|
10
|
+
// Ensure ~/.local/bin and common paths are on PATH for spawned processes
|
|
11
|
+
const EXTENDED_ENV = {
|
|
12
|
+
...process.env,
|
|
13
|
+
HOME: os.homedir(),
|
|
14
|
+
PATH: [
|
|
15
|
+
path.join(os.homedir(), '.local', 'bin'),
|
|
16
|
+
path.join(os.homedir(), '.npm-global', 'bin'),
|
|
17
|
+
'/usr/local/bin',
|
|
18
|
+
process.env.PATH || '',
|
|
19
|
+
].join(':'),
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
function spawnAsync(command, args, options = {}) {
|
|
23
|
+
return new Promise((resolve, reject) => {
|
|
24
|
+
const child = spawn(command, args, {
|
|
25
|
+
...options,
|
|
26
|
+
shell: false,
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
let stdout = '';
|
|
30
|
+
let stderr = '';
|
|
31
|
+
|
|
32
|
+
child.stdout.on('data', (data) => {
|
|
33
|
+
stdout += data.toString();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
child.stderr.on('data', (data) => {
|
|
37
|
+
stderr += data.toString();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
child.on('error', (error) => {
|
|
41
|
+
reject(error);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
child.on('close', (code) => {
|
|
45
|
+
if (code === 0) {
|
|
46
|
+
resolve({ stdout, stderr });
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
const error = new Error(`Command failed: ${command} ${args.join(' ')}`);
|
|
50
|
+
error.code = code;
|
|
51
|
+
error.stdout = stdout;
|
|
52
|
+
error.stderr = stderr;
|
|
53
|
+
reject(error);
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// POST /api/community-tools/configure
|
|
59
|
+
// Body: { projectPath, mcpBackend, apiKeys, gpuConfig }
|
|
60
|
+
router.post('/configure', async (req, res) => {
|
|
61
|
+
try {
|
|
62
|
+
if (!req.body || typeof req.body !== 'object') {
|
|
63
|
+
return res.status(400).json({ error: 'Invalid request body' });
|
|
64
|
+
}
|
|
65
|
+
const { projectPath, mcpBackend, apiKeys, gpuConfig } = req.body;
|
|
66
|
+
|
|
67
|
+
// Resolve project path: use provided path, or fall back to server's working directory
|
|
68
|
+
// (which is typically the VibeLab install directory)
|
|
69
|
+
let resolvedPath;
|
|
70
|
+
if (projectPath && projectPath !== '.') {
|
|
71
|
+
resolvedPath = path.resolve(projectPath);
|
|
72
|
+
} else {
|
|
73
|
+
// Use __dirname parent (VibeLab root) as default
|
|
74
|
+
// community-tools.js is in server/routes/ — go up 2 levels to VibeLab root
|
|
75
|
+
resolvedPath = path.resolve(path.join(path.dirname(new URL(import.meta.url).pathname), '..', '..'));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const results = { steps: [], errors: [] };
|
|
79
|
+
|
|
80
|
+
// ── Step 1: Write API keys to ~/.openclaw/community-tools.json ──
|
|
81
|
+
// (NOT project .env — writing .env triggers Vite restart and kills the fetch)
|
|
82
|
+
if (apiKeys && Object.keys(apiKeys).length > 0) {
|
|
83
|
+
try {
|
|
84
|
+
const configDir = path.join(os.homedir(), '.openclaw');
|
|
85
|
+
const configPath = path.join(configDir, 'community-tools.json');
|
|
86
|
+
await fs.mkdir(configDir, { recursive: true });
|
|
87
|
+
|
|
88
|
+
let existing = {};
|
|
89
|
+
try {
|
|
90
|
+
existing = JSON.parse(await fs.readFile(configPath, 'utf8'));
|
|
91
|
+
} catch {
|
|
92
|
+
// file doesn't exist yet
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Merge keys into config
|
|
96
|
+
const envKeys = existing.envKeys || {};
|
|
97
|
+
for (const [key, value] of Object.entries(apiKeys)) {
|
|
98
|
+
if (value && value.trim()) {
|
|
99
|
+
envKeys[key] = value.trim();
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
existing.envKeys = envKeys;
|
|
103
|
+
|
|
104
|
+
await fs.writeFile(configPath, JSON.stringify(existing, null, 2), { mode: 0o600 });
|
|
105
|
+
|
|
106
|
+
// Also write a shell-sourceable file for convenience
|
|
107
|
+
// Escape values: replace \ with \\, " with \", $ with \$, ` with \`
|
|
108
|
+
const shellEscape = (s) => s.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\$/g, '\\$').replace(/`/g, '\\`');
|
|
109
|
+
const rcPath = path.join(configDir, 'community-tools.env');
|
|
110
|
+
const rcContent = Object.entries(envKeys)
|
|
111
|
+
.map(([k, v]) => `export ${k}="${shellEscape(v)}"`)
|
|
112
|
+
.join('\n') + '\n';
|
|
113
|
+
await fs.writeFile(rcPath, rcContent, { mode: 0o600 });
|
|
114
|
+
|
|
115
|
+
results.steps.push({ step: 'env', status: 'ok', message: `Saved ${Object.keys(apiKeys).length} key(s) to ~/.openclaw/community-tools.json` });
|
|
116
|
+
} catch (err) {
|
|
117
|
+
results.errors.push({ step: 'env', error: err.message });
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// ── Step 2: Register MCP server ──
|
|
122
|
+
if (mcpBackend) {
|
|
123
|
+
try {
|
|
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 {
|
|
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` });
|
|
150
|
+
} else {
|
|
151
|
+
await spawnAsync('claude', args, { env: EXTENDED_ENV });
|
|
152
|
+
results.steps.push({ step: 'mcp', status: 'ok', message: `Registered ${mcpBackend} MCP server` });
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Install codex CLI if needed
|
|
157
|
+
if (mcpBackend === 'codex') {
|
|
158
|
+
try {
|
|
159
|
+
await spawnAsync('which', ['codex']);
|
|
160
|
+
results.steps.push({ step: 'codex-cli', status: 'skipped', message: 'Codex CLI already installed' });
|
|
161
|
+
} catch {
|
|
162
|
+
try {
|
|
163
|
+
await spawnAsync('npm', ['install', '-g', '@openai/codex'], { env: EXTENDED_ENV });
|
|
164
|
+
results.steps.push({ step: 'codex-cli', status: 'ok', message: 'Installed Codex CLI' });
|
|
165
|
+
} catch (err) {
|
|
166
|
+
results.errors.push({ step: 'codex-cli', error: `Failed to install codex: ${err.message}` });
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
} catch (err) {
|
|
171
|
+
results.errors.push({ step: 'mcp', error: err.message });
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// ── Step 3: Write GPU config to CLAUDE.md ──
|
|
176
|
+
if (gpuConfig) {
|
|
177
|
+
try {
|
|
178
|
+
const claudeMdPath = path.join(resolvedPath, 'CLAUDE.md');
|
|
179
|
+
let claudeContent = '';
|
|
180
|
+
try {
|
|
181
|
+
claudeContent = await fs.readFile(claudeMdPath, 'utf8');
|
|
182
|
+
} catch {
|
|
183
|
+
// CLAUDE.md doesn't exist
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Check if GPU section already exists
|
|
187
|
+
if (claudeContent.includes('gpu:')) {
|
|
188
|
+
results.steps.push({ step: 'gpu', status: 'skipped', message: 'GPU config already exists in CLAUDE.md' });
|
|
189
|
+
} else {
|
|
190
|
+
const separator = claudeContent.trim() ? '\n\n' : '';
|
|
191
|
+
await fs.writeFile(claudeMdPath, claudeContent + separator + gpuConfig.trim() + '\n', 'utf8');
|
|
192
|
+
results.steps.push({ step: 'gpu', status: 'ok', message: 'Added GPU config to CLAUDE.md' });
|
|
193
|
+
}
|
|
194
|
+
} catch (err) {
|
|
195
|
+
results.errors.push({ step: 'gpu', error: err.message });
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// ── Step 4: Symlink skills ──
|
|
200
|
+
try {
|
|
201
|
+
const skillsDir = path.join(resolvedPath, 'skills');
|
|
202
|
+
const claudeSkillsDir = path.join(os.homedir(), '.claude', 'skills');
|
|
203
|
+
await fs.mkdir(claudeSkillsDir, { recursive: true });
|
|
204
|
+
|
|
205
|
+
let linked = 0;
|
|
206
|
+
const entries = await fs.readdir(skillsDir, { withFileTypes: true });
|
|
207
|
+
for (const entry of entries) {
|
|
208
|
+
if (entry.isDirectory() && (entry.name.startsWith('aris-') || entry.name.startsWith('inno-') || entry.name === 'autoresearch')) {
|
|
209
|
+
const target = path.join(claudeSkillsDir, entry.name);
|
|
210
|
+
try {
|
|
211
|
+
await fs.access(target);
|
|
212
|
+
// already exists
|
|
213
|
+
} catch {
|
|
214
|
+
await fs.symlink(path.join(skillsDir, entry.name), target);
|
|
215
|
+
linked++;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
results.steps.push({ step: 'skills', status: 'ok', message: `${linked} new skill(s) linked` });
|
|
220
|
+
} catch (err) {
|
|
221
|
+
results.errors.push({ step: 'skills', error: err.message });
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const success = results.errors.length === 0;
|
|
225
|
+
res.json({ success, ...results });
|
|
226
|
+
} catch (error) {
|
|
227
|
+
console.error('Community tools configure error:', error);
|
|
228
|
+
res.status(500).json({ error: 'Configuration failed. Check server logs for details.' });
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
// GET /api/community-tools/status
|
|
233
|
+
router.get('/status', async (req, res) => {
|
|
234
|
+
try {
|
|
235
|
+
const projectPath = req.query.path;
|
|
236
|
+
if (!projectPath) {
|
|
237
|
+
return res.status(400).json({ error: 'path query param required' });
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const resolvedPath = path.resolve(projectPath);
|
|
241
|
+
// Validate path is within home directory to prevent path traversal
|
|
242
|
+
if (!resolvedPath.startsWith(os.homedir())) {
|
|
243
|
+
return res.status(403).json({ error: 'Invalid path' });
|
|
244
|
+
}
|
|
245
|
+
const status = {
|
|
246
|
+
mcpRegistered: {},
|
|
247
|
+
envKeys: {},
|
|
248
|
+
gpuConfigured: false,
|
|
249
|
+
skillsLinked: 0,
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
// Check MCP servers
|
|
253
|
+
try {
|
|
254
|
+
const { stdout } = await spawnAsync('claude', ['mcp', 'list'], { env: EXTENDED_ENV });
|
|
255
|
+
status.mcpRegistered.codex = stdout.includes('codex');
|
|
256
|
+
status.mcpRegistered['llm-chat'] = stdout.includes('llm-chat');
|
|
257
|
+
status.mcpRegistered.gemini = stdout.includes('gemini-review');
|
|
258
|
+
} catch {
|
|
259
|
+
// claude CLI not available
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Check .env keys
|
|
263
|
+
try {
|
|
264
|
+
const envContent = await fs.readFile(path.join(resolvedPath, '.env'), 'utf8');
|
|
265
|
+
status.envKeys.OPENAI_API_KEY = envContent.includes('OPENAI_API_KEY=') && !envContent.includes('OPENAI_API_KEY=\n');
|
|
266
|
+
status.envKeys.LLM_API_KEY = envContent.includes('LLM_API_KEY=') && !envContent.includes('LLM_API_KEY=\n');
|
|
267
|
+
status.envKeys.GEMINI_API_KEY = envContent.includes('GEMINI_API_KEY=') && !envContent.includes('GEMINI_API_KEY=\n');
|
|
268
|
+
} catch {
|
|
269
|
+
// no .env
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Check CLAUDE.md GPU config
|
|
273
|
+
try {
|
|
274
|
+
const claudeContent = await fs.readFile(path.join(resolvedPath, 'CLAUDE.md'), 'utf8');
|
|
275
|
+
status.gpuConfigured = claudeContent.includes('gpu:');
|
|
276
|
+
} catch {
|
|
277
|
+
// no CLAUDE.md
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Check skill symlinks
|
|
281
|
+
try {
|
|
282
|
+
const claudeSkillsDir = path.join(os.homedir(), '.claude', 'skills');
|
|
283
|
+
const entries = await fs.readdir(claudeSkillsDir);
|
|
284
|
+
status.skillsLinked = entries.filter((e) => e.startsWith('aris-')).length;
|
|
285
|
+
} catch {
|
|
286
|
+
// no skills dir
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
res.json(status);
|
|
290
|
+
} catch (error) {
|
|
291
|
+
res.status(500).json({ error: 'Internal server error' });
|
|
292
|
+
}
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
// GET /api/community-tools/compute-nodes — list available compute nodes for GPU config
|
|
296
|
+
router.get('/compute-nodes', async (req, res) => {
|
|
297
|
+
try {
|
|
298
|
+
const config = await loadAllNodes();
|
|
299
|
+
const nodes = (config.nodes || []).map((n) => ({
|
|
300
|
+
id: n.id,
|
|
301
|
+
name: n.name || n.host,
|
|
302
|
+
host: n.host,
|
|
303
|
+
user: n.user,
|
|
304
|
+
port: n.port || 22,
|
|
305
|
+
workDir: n.workDir || '~',
|
|
306
|
+
type: n.type || 'direct',
|
|
307
|
+
gpuInfo: n.gpuInfo || null,
|
|
308
|
+
}));
|
|
309
|
+
res.json({ nodes, activeNodeId: config.activeNodeId });
|
|
310
|
+
} catch (error) {
|
|
311
|
+
res.status(500).json({ error: 'Internal server error' });
|
|
312
|
+
}
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
// POST /api/community-tools/detect-gpu — detect GPU info from a compute node and return CLAUDE.md template
|
|
316
|
+
router.post('/detect-gpu', async (req, res) => {
|
|
317
|
+
try {
|
|
318
|
+
const { nodeId } = req.body;
|
|
319
|
+
if (!nodeId) {
|
|
320
|
+
return res.status(400).json({ error: 'nodeId is required' });
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const node = await loadNodeConfig(nodeId);
|
|
324
|
+
let gpuOutput = '';
|
|
325
|
+
|
|
326
|
+
try {
|
|
327
|
+
gpuOutput = await ComputeNode.run({
|
|
328
|
+
nodeId,
|
|
329
|
+
command: 'nvidia-smi --query-gpu=index,name,memory.total --format=csv,noheader 2>/dev/null || echo "NO_GPU"',
|
|
330
|
+
});
|
|
331
|
+
} catch (err) {
|
|
332
|
+
gpuOutput = 'NO_GPU';
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// Parse GPU info
|
|
336
|
+
let gpuLine = 'No GPU detected';
|
|
337
|
+
const gpuLines = gpuOutput.trim().split('\n').filter((l) => l && !l.includes('NO_GPU'));
|
|
338
|
+
|
|
339
|
+
if (gpuLines.length > 0) {
|
|
340
|
+
// Parse: "0, NVIDIA A100-SXM4-80GB, 81920 MiB"
|
|
341
|
+
const gpus = gpuLines.map((line) => {
|
|
342
|
+
const parts = line.split(',').map((s) => s.trim());
|
|
343
|
+
const name = (parts[1] || 'GPU').replace('NVIDIA ', '');
|
|
344
|
+
const memMb = parseInt(parts[2]) || 0;
|
|
345
|
+
const memGb = Math.round(memMb / 1024);
|
|
346
|
+
return { name, memGb };
|
|
347
|
+
});
|
|
348
|
+
const firstName = gpus[0]?.name || 'GPU';
|
|
349
|
+
const firstMem = gpus[0]?.memGb || 0;
|
|
350
|
+
gpuLine = `${gpus.length}x ${firstName}${firstMem ? ` (${firstMem}GB)` : ''}`;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// Detect conda
|
|
354
|
+
let condaEnv = '';
|
|
355
|
+
try {
|
|
356
|
+
const condaOut = await ComputeNode.run({
|
|
357
|
+
nodeId,
|
|
358
|
+
command: 'conda info --envs 2>/dev/null | grep "\\*" | head -1 || echo ""',
|
|
359
|
+
});
|
|
360
|
+
const match = condaOut.trim().match(/^(\S+)/);
|
|
361
|
+
if (match && match[1] !== '') {
|
|
362
|
+
condaEnv = match[1];
|
|
363
|
+
}
|
|
364
|
+
} catch {
|
|
365
|
+
// no conda
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// Build SSH command — sanitize user/host to prevent shell injection
|
|
369
|
+
const safeStr = (s) => String(s || '').replace(/[^a-zA-Z0-9._@\-]/g, '');
|
|
370
|
+
const sshCmd = node.port && node.port !== 22
|
|
371
|
+
? `ssh -p ${parseInt(node.port) || 22} ${safeStr(node.user)}@${safeStr(node.host)}`
|
|
372
|
+
: `ssh ${safeStr(node.user)}@${safeStr(node.host)}`;
|
|
373
|
+
|
|
374
|
+
// Generate CLAUDE.md template
|
|
375
|
+
const template = [
|
|
376
|
+
'## Remote Server',
|
|
377
|
+
'- gpu: remote',
|
|
378
|
+
`- SSH: \`${sshCmd}\``,
|
|
379
|
+
`- GPU: ${gpuLine}`,
|
|
380
|
+
condaEnv ? `- Conda: \`conda activate ${condaEnv}\`` : '- Conda: `conda activate your_env`',
|
|
381
|
+
`- Code dir: \`${node.workDir || '~/experiments'}\``,
|
|
382
|
+
'- code_sync: rsync',
|
|
383
|
+
].join('\n');
|
|
384
|
+
|
|
385
|
+
res.json({
|
|
386
|
+
success: true,
|
|
387
|
+
nodeId: node.id,
|
|
388
|
+
nodeName: node.name || node.host,
|
|
389
|
+
gpuLine,
|
|
390
|
+
template,
|
|
391
|
+
});
|
|
392
|
+
} catch (error) {
|
|
393
|
+
console.error('GPU detect error:', error);
|
|
394
|
+
res.status(500).json({ error: 'Internal server error' });
|
|
395
|
+
}
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
export default router;
|
package/server/routes/compute.js
CHANGED
|
@@ -185,6 +185,79 @@ router.post('/nodes/:id/run', async (req, res) => {
|
|
|
185
185
|
}
|
|
186
186
|
});
|
|
187
187
|
|
|
188
|
+
// GET /api/compute/nodes/:id/monitor - Get GPU and CPU usage
|
|
189
|
+
router.get('/nodes/:id/monitor', async (req, res) => {
|
|
190
|
+
try {
|
|
191
|
+
const monitorCmd = [
|
|
192
|
+
'echo "===GPU_START==="',
|
|
193
|
+
'(nvidia-smi --query-gpu=index,name,utilization.gpu,utilization.memory,memory.used,memory.total,temperature.gpu,power.draw --format=csv,noheader,nounits 2>/dev/null || echo "NO_GPU")',
|
|
194
|
+
'echo "===GPU_END==="',
|
|
195
|
+
'echo "===CPU_START==="',
|
|
196
|
+
'nproc 2>/dev/null || echo "0"',
|
|
197
|
+
'echo "---"',
|
|
198
|
+
// 1-second load averages; top is more portable than mpstat
|
|
199
|
+
'(cat /proc/loadavg 2>/dev/null || uptime | sed "s/.*load average[s]*: //" | cut -d, -f1)',
|
|
200
|
+
'echo "---"',
|
|
201
|
+
// Memory info in MB
|
|
202
|
+
'(free -m 2>/dev/null | grep Mem || echo "Mem: 0 0 0 0 0 0")',
|
|
203
|
+
'echo "===CPU_END==="',
|
|
204
|
+
].join(' && ');
|
|
205
|
+
|
|
206
|
+
const output = await ComputeNode.run({
|
|
207
|
+
nodeId: req.params.id,
|
|
208
|
+
command: monitorCmd,
|
|
209
|
+
skipSync: true,
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
const gpuSection = (output.match(/===GPU_START===\n([\s\S]*?)===GPU_END===/) || [])[1]?.trim() || '';
|
|
213
|
+
const cpuSection = (output.match(/===CPU_START===\n([\s\S]*?)===CPU_END===/) || [])[1]?.trim() || '';
|
|
214
|
+
|
|
215
|
+
const gpus = [];
|
|
216
|
+
if (gpuSection && gpuSection !== 'NO_GPU') {
|
|
217
|
+
for (const line of gpuSection.split('\n')) {
|
|
218
|
+
const parts = line.split(',').map(s => s.trim());
|
|
219
|
+
if (parts.length >= 7) {
|
|
220
|
+
gpus.push({
|
|
221
|
+
index: parseInt(parts[0]) || 0,
|
|
222
|
+
name: parts[1] || 'Unknown',
|
|
223
|
+
gpuUtil: parseFloat(parts[2]) || 0,
|
|
224
|
+
memUtil: parseFloat(parts[3]) || 0,
|
|
225
|
+
memUsedMB: parseFloat(parts[4]) || 0,
|
|
226
|
+
memTotalMB: parseFloat(parts[5]) || 0,
|
|
227
|
+
tempC: parseFloat(parts[6]) || 0,
|
|
228
|
+
powerW: parseFloat(parts[7]) || 0,
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const cpuParts = cpuSection.split('---').map(s => s.trim());
|
|
235
|
+
const cpuCount = parseInt(cpuParts[0]) || 0;
|
|
236
|
+
const loadAvg = parseFloat(cpuParts[1]) || 0;
|
|
237
|
+
const memLine = cpuParts[2] || '';
|
|
238
|
+
const memTokens = memLine.split(/\s+/);
|
|
239
|
+
const memTotalMB = parseInt(memTokens[1]) || 0;
|
|
240
|
+
const memUsedMB = parseInt(memTokens[2]) || 0;
|
|
241
|
+
|
|
242
|
+
res.json({
|
|
243
|
+
success: true,
|
|
244
|
+
gpus,
|
|
245
|
+
cpu: {
|
|
246
|
+
cores: cpuCount,
|
|
247
|
+
loadAvg,
|
|
248
|
+
utilPercent: cpuCount > 0 ? Math.min(100, Math.round((loadAvg / cpuCount) * 100)) : 0,
|
|
249
|
+
memTotalMB,
|
|
250
|
+
memUsedMB,
|
|
251
|
+
memUtilPercent: memTotalMB > 0 ? Math.round((memUsedMB / memTotalMB) * 100) : 0,
|
|
252
|
+
},
|
|
253
|
+
timestamp: Date.now(),
|
|
254
|
+
});
|
|
255
|
+
} catch (error) {
|
|
256
|
+
console.error('Error monitoring compute node:', error);
|
|
257
|
+
res.json({ success: false, error: error.message, gpus: [], cpu: null, timestamp: Date.now() });
|
|
258
|
+
}
|
|
259
|
+
});
|
|
260
|
+
|
|
188
261
|
// ─── Slurm endpoints ───
|
|
189
262
|
|
|
190
263
|
// GET /api/compute/nodes/:id/slurm/info - Get partition info
|
|
@@ -340,6 +413,88 @@ router.post('/run', async (req, res) => {
|
|
|
340
413
|
}
|
|
341
414
|
});
|
|
342
415
|
|
|
416
|
+
// GET /api/compute/local/monitor - Monitor the local machine's CPU and GPU
|
|
417
|
+
router.get('/local/monitor', async (_req, res) => {
|
|
418
|
+
const { exec } = await import('child_process');
|
|
419
|
+
const os = await import('os');
|
|
420
|
+
const { promisify } = await import('util');
|
|
421
|
+
const execAsync = promisify(exec);
|
|
422
|
+
|
|
423
|
+
const platform = os.default.platform();
|
|
424
|
+
|
|
425
|
+
const run = async (cmd) => {
|
|
426
|
+
try {
|
|
427
|
+
const { stdout } = await execAsync(cmd, { timeout: 10000 });
|
|
428
|
+
return stdout.trim();
|
|
429
|
+
} catch {
|
|
430
|
+
return '';
|
|
431
|
+
}
|
|
432
|
+
};
|
|
433
|
+
|
|
434
|
+
try {
|
|
435
|
+
// --- GPU ---
|
|
436
|
+
const gpus = [];
|
|
437
|
+
const gpuRaw = await run(
|
|
438
|
+
'nvidia-smi --query-gpu=index,name,utilization.gpu,utilization.memory,memory.used,memory.total,temperature.gpu,power.draw --format=csv,noheader,nounits'
|
|
439
|
+
);
|
|
440
|
+
if (gpuRaw) {
|
|
441
|
+
for (const line of gpuRaw.split('\n')) {
|
|
442
|
+
const p = line.split(',').map(s => s.trim());
|
|
443
|
+
if (p.length >= 7) {
|
|
444
|
+
gpus.push({
|
|
445
|
+
index: parseInt(p[0]) || 0,
|
|
446
|
+
name: p[1] || 'Unknown',
|
|
447
|
+
gpuUtil: parseFloat(p[2]) || 0,
|
|
448
|
+
memUtil: parseFloat(p[3]) || 0,
|
|
449
|
+
memUsedMB: parseFloat(p[4]) || 0,
|
|
450
|
+
memTotalMB: parseFloat(p[5]) || 0,
|
|
451
|
+
tempC: parseFloat(p[6]) || 0,
|
|
452
|
+
powerW: parseFloat(p[7]) || 0,
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// --- CPU ---
|
|
459
|
+
const cpuCores = os.default.cpus().length;
|
|
460
|
+
const loadAvg = os.default.loadavg()[0]; // 1-min
|
|
461
|
+
const totalMemMB = Math.round(os.default.totalmem() / (1024 * 1024));
|
|
462
|
+
const freeMemMB = Math.round(os.default.freemem() / (1024 * 1024));
|
|
463
|
+
const usedMemMB = totalMemMB - freeMemMB;
|
|
464
|
+
|
|
465
|
+
let cpuUtilPercent = Math.min(100, Math.round((loadAvg / cpuCores) * 100));
|
|
466
|
+
|
|
467
|
+
// On macOS, try to get a more accurate CPU usage from top
|
|
468
|
+
if (platform === 'darwin') {
|
|
469
|
+
const topOut = await run("top -l 1 -n 0 | head -4");
|
|
470
|
+
const cpuMatch = topOut.match(/CPU usage:\s+(\d+(?:\.\d+)?)% user,\s+(\d+(?:\.\d+)?)% sys/);
|
|
471
|
+
if (cpuMatch) {
|
|
472
|
+
cpuUtilPercent = Math.round(parseFloat(cpuMatch[1]) + parseFloat(cpuMatch[2]));
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
res.json({
|
|
477
|
+
success: true,
|
|
478
|
+
hostname: os.default.hostname(),
|
|
479
|
+
platform,
|
|
480
|
+
gpus,
|
|
481
|
+
cpu: {
|
|
482
|
+
cores: cpuCores,
|
|
483
|
+
model: os.default.cpus()[0]?.model || 'Unknown',
|
|
484
|
+
loadAvg,
|
|
485
|
+
utilPercent: cpuUtilPercent,
|
|
486
|
+
memTotalMB: totalMemMB,
|
|
487
|
+
memUsedMB: usedMemMB,
|
|
488
|
+
memUtilPercent: totalMemMB > 0 ? Math.round((usedMemMB / totalMemMB) * 100) : 0,
|
|
489
|
+
},
|
|
490
|
+
timestamp: Date.now(),
|
|
491
|
+
});
|
|
492
|
+
} catch (error) {
|
|
493
|
+
console.error('Error monitoring local machine:', error);
|
|
494
|
+
res.json({ success: false, error: error.message, gpus: [], cpu: null, timestamp: Date.now() });
|
|
495
|
+
}
|
|
496
|
+
});
|
|
497
|
+
|
|
343
498
|
router.get('/status', async (req, res) => {
|
|
344
499
|
try {
|
|
345
500
|
const node = await getActiveNode();
|
package/server/routes/git.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import express from 'express';
|
|
2
|
-
import {
|
|
3
|
-
import { promisify } from 'util';
|
|
2
|
+
import { spawn } from 'child_process';
|
|
4
3
|
import path from 'path';
|
|
5
4
|
import { promises as fs } from 'fs';
|
|
6
5
|
import { extractProjectDirectory } from '../projects.js';
|
|
@@ -8,7 +7,6 @@ import { queryClaudeSDK } from '../claude-sdk.js';
|
|
|
8
7
|
import { spawnCursor } from '../cursor-cli.js';
|
|
9
8
|
|
|
10
9
|
const router = express.Router();
|
|
11
|
-
const execAsync = promisify(exec);
|
|
12
10
|
|
|
13
11
|
function spawnAsync(command, args, options = {}) {
|
|
14
12
|
return new Promise((resolve, reject) => {
|
|
@@ -98,14 +96,14 @@ async function validateGitRepository(projectPath) {
|
|
|
98
96
|
|
|
99
97
|
try {
|
|
100
98
|
// Allow any directory that is inside a work tree (repo root or nested folder).
|
|
101
|
-
const { stdout: insideWorkTreeOutput } = await
|
|
99
|
+
const { stdout: insideWorkTreeOutput } = await spawnAsync('git', ['rev-parse', '--is-inside-work-tree'], { cwd: projectPath });
|
|
102
100
|
const isInsideWorkTree = insideWorkTreeOutput.trim() === 'true';
|
|
103
101
|
if (!isInsideWorkTree) {
|
|
104
102
|
throw new Error('Not inside a git work tree');
|
|
105
103
|
}
|
|
106
104
|
|
|
107
105
|
// Ensure git can resolve the repository root for this directory.
|
|
108
|
-
await
|
|
106
|
+
await spawnAsync('git', ['rev-parse', '--show-toplevel'], { cwd: projectPath });
|
|
109
107
|
} catch {
|
|
110
108
|
throw new Error('Not a git repository. This directory does not contain a .git folder. Initialize a git repository with "git init" to use source control features.');
|
|
111
109
|
}
|
|
@@ -274,7 +272,7 @@ router.get('/status', async (req, res) => {
|
|
|
274
272
|
}
|
|
275
273
|
|
|
276
274
|
// Get git status
|
|
277
|
-
const { stdout: statusOutput } = await
|
|
275
|
+
const { stdout: statusOutput } = await spawnAsync('git', ['status', '--porcelain'], { cwd: projectPath });
|
|
278
276
|
|
|
279
277
|
const modified = [];
|
|
280
278
|
const added = [];
|
|
@@ -565,7 +563,7 @@ router.get('/branches', async (req, res) => {
|
|
|
565
563
|
}
|
|
566
564
|
|
|
567
565
|
// Get all branches
|
|
568
|
-
const { stdout } = await
|
|
566
|
+
const { stdout } = await spawnAsync('git', ['branch', '-a'], { cwd: projectPath });
|
|
569
567
|
|
|
570
568
|
// Parse branches
|
|
571
569
|
const branches = stdout
|
|
@@ -676,8 +674,8 @@ router.get('/commits', async (req, res) => {
|
|
|
676
674
|
// Get stats for each commit
|
|
677
675
|
for (const commit of commits) {
|
|
678
676
|
try {
|
|
679
|
-
const { stdout: stats } = await
|
|
680
|
-
|
|
677
|
+
const { stdout: stats } = await spawnAsync(
|
|
678
|
+
'git', ['show', '--stat', '--format=', commit.hash],
|
|
681
679
|
{ cwd: projectPath }
|
|
682
680
|
);
|
|
683
681
|
commit.stats = stats.trim().split('\n').pop(); // Get the summary line
|
|
@@ -705,8 +703,8 @@ router.get('/commit-diff', async (req, res) => {
|
|
|
705
703
|
const projectPath = await getActualProjectPath(project);
|
|
706
704
|
|
|
707
705
|
// Get diff for the commit
|
|
708
|
-
const { stdout } = await
|
|
709
|
-
|
|
706
|
+
const { stdout } = await spawnAsync(
|
|
707
|
+
'git', ['show', commit],
|
|
710
708
|
{ cwd: projectPath }
|
|
711
709
|
);
|
|
712
710
|
|