opencode-studio-server 1.0.13 → 1.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/index.js +108 -12
  2. package/package.json +1 -1
package/index.js CHANGED
@@ -318,7 +318,6 @@ app.get('/api/plugins', (req, res) => {
318
318
 
319
319
  for (const entry of entries) {
320
320
  if (entry.isDirectory()) {
321
- // Check for index.js or index.ts
322
321
  const jsPath = path.join(pluginDir, entry.name, 'index.js');
323
322
  const tsPath = path.join(pluginDir, entry.name, 'index.ts');
324
323
 
@@ -329,6 +328,12 @@ app.get('/api/plugins', (req, res) => {
329
328
  enabled: true
330
329
  });
331
330
  }
331
+ } else if (entry.isFile() && (entry.name.endsWith('.js') || entry.name.endsWith('.ts'))) {
332
+ plugins.push({
333
+ name: entry.name.replace(/\.(js|ts)$/, ''),
334
+ path: path.join(pluginDir, entry.name),
335
+ enabled: true
336
+ });
332
337
  }
333
338
  }
334
339
  res.json(plugins);
@@ -350,6 +355,12 @@ app.get('/api/plugins/:name', (req, res) => {
350
355
  } else if (fs.existsSync(path.join(dirPath, 'index.ts'))) {
351
356
  content = fs.readFileSync(path.join(dirPath, 'index.ts'), 'utf8');
352
357
  filename = 'index.ts';
358
+ } else if (fs.existsSync(path.join(pluginDir, name + '.js'))) {
359
+ content = fs.readFileSync(path.join(pluginDir, name + '.js'), 'utf8');
360
+ filename = name + '.js';
361
+ } else if (fs.existsSync(path.join(pluginDir, name + '.ts'))) {
362
+ content = fs.readFileSync(path.join(pluginDir, name + '.ts'), 'utf8');
363
+ filename = name + '.ts';
353
364
  } else {
354
365
  return res.status(404).json({ error: 'Plugin not found' });
355
366
  }
@@ -384,6 +395,10 @@ app.delete('/api/plugins/:name', (req, res) => {
384
395
 
385
396
  if (fs.existsSync(dirPath)) {
386
397
  fs.rmSync(dirPath, { recursive: true, force: true });
398
+ } else if (fs.existsSync(path.join(pluginDir, name + '.js'))) {
399
+ fs.unlinkSync(path.join(pluginDir, name + '.js'));
400
+ } else if (fs.existsSync(path.join(pluginDir, name + '.ts'))) {
401
+ fs.unlinkSync(path.join(pluginDir, name + '.ts'));
387
402
  }
388
403
  res.json({ success: true });
389
404
  });
@@ -729,6 +744,7 @@ app.get('/api/debug/paths', (req, res) => {
729
744
 
730
745
  app.get('/api/usage', async (req, res) => {
731
746
  try {
747
+ const { projectId: filterProjectId, granularity = 'daily' } = req.query;
732
748
  const home = os.homedir();
733
749
  const candidatePaths = [
734
750
  process.env.LOCALAPPDATA ? path.join(process.env.LOCALAPPDATA, 'opencode', 'storage', 'message') : null,
@@ -736,16 +752,60 @@ app.get('/api/usage', async (req, res) => {
736
752
  path.join(home, '.opencode', 'storage', 'message')
737
753
  ].filter(p => p && fs.existsSync(p));
738
754
 
755
+ const getSessionDir = (messageDir) => {
756
+ return path.join(path.dirname(messageDir), 'session');
757
+ };
758
+
759
+ const sessionProjectMap = new Map();
760
+
761
+ const loadProjects = (messageDir) => {
762
+ const sessionDir = getSessionDir(messageDir);
763
+ if (!fs.existsSync(sessionDir)) return;
764
+
765
+ try {
766
+ const projectDirs = fs.readdirSync(sessionDir);
767
+
768
+ for (const projDir of projectDirs) {
769
+ const fullProjPath = path.join(sessionDir, projDir);
770
+ if (!fs.statSync(fullProjPath).isDirectory()) continue;
771
+
772
+ const files = fs.readdirSync(fullProjPath);
773
+ for (const file of files) {
774
+ if (file.startsWith('ses_') && file.endsWith('.json')) {
775
+ const sessionId = file.replace('.json', '');
776
+ try {
777
+ const meta = JSON.parse(fs.readFileSync(path.join(fullProjPath, file), 'utf8'));
778
+ let projectName = 'Unknown Project';
779
+ if (meta.directory) {
780
+ projectName = path.basename(meta.directory);
781
+ } else if (meta.projectID) {
782
+ projectName = meta.projectID.substring(0, 8);
783
+ }
784
+ sessionProjectMap.set(sessionId, { name: projectName, id: meta.projectID || projDir });
785
+ } catch (e) {}
786
+ }
787
+ }
788
+ }
789
+ } catch (e) {
790
+ console.error('Error loading projects:', e);
791
+ }
792
+ };
793
+
794
+ for (const logDir of candidatePaths) {
795
+ loadProjects(logDir);
796
+ }
797
+
739
798
  const stats = {
740
799
  totalCost: 0,
741
800
  totalTokens: 0,
742
801
  byModel: {},
743
- byDay: {}
802
+ byTime: {},
803
+ byProject: {}
744
804
  };
745
805
 
746
806
  const processedFiles = new Set();
747
807
 
748
- const processMessage = (filePath) => {
808
+ const processMessage = (filePath, sessionId) => {
749
809
  if (processedFiles.has(filePath)) return;
750
810
  processedFiles.add(filePath);
751
811
 
@@ -754,27 +814,62 @@ app.get('/api/usage', async (req, res) => {
754
814
  const msg = JSON.parse(content);
755
815
 
756
816
  const model = msg.modelID || (msg.model && (msg.model.modelID || msg.model.id)) || 'unknown';
817
+ const projectInfo = sessionProjectMap.get(sessionId) || { name: 'Unassigned', id: 'unknown' };
818
+ const projectId = projectInfo.id || 'unknown';
819
+
820
+ if (filterProjectId && filterProjectId !== 'all' && projectId !== filterProjectId) {
821
+ return;
822
+ }
757
823
 
758
824
  if (msg.role === 'assistant' && msg.tokens) {
759
825
  const cost = msg.cost || 0;
760
- const tokens = (msg.tokens.input || 0) + (msg.tokens.output || 0);
761
- const date = new Date(msg.time.created).toISOString().split('T')[0];
826
+ const inputTokens = msg.tokens.input || 0;
827
+ const outputTokens = msg.tokens.output || 0;
828
+ const tokens = inputTokens + outputTokens;
829
+
830
+ const timestamp = msg.time.created;
831
+ const dateObj = new Date(timestamp);
832
+ let timeKey;
833
+
834
+ if (granularity === 'hourly') {
835
+ timeKey = dateObj.toISOString().substring(0, 13) + ':00:00Z';
836
+ } else if (granularity === 'weekly') {
837
+ const day = dateObj.getDay();
838
+ const diff = dateObj.getDate() - day + (day === 0 ? -6 : 1);
839
+ const monday = new Date(dateObj.setDate(diff));
840
+ timeKey = monday.toISOString().split('T')[0];
841
+ } else {
842
+ timeKey = dateObj.toISOString().split('T')[0];
843
+ }
762
844
 
763
845
  if (tokens > 0) {
764
846
  stats.totalCost += cost;
765
847
  stats.totalTokens += tokens;
766
848
 
767
849
  if (!stats.byModel[model]) {
768
- stats.byModel[model] = { name: model, cost: 0, tokens: 0 };
850
+ stats.byModel[model] = { name: model, cost: 0, tokens: 0, inputTokens: 0, outputTokens: 0 };
769
851
  }
770
852
  stats.byModel[model].cost += cost;
771
853
  stats.byModel[model].tokens += tokens;
854
+ stats.byModel[model].inputTokens += inputTokens;
855
+ stats.byModel[model].outputTokens += outputTokens;
772
856
 
773
- if (!stats.byDay[date]) {
774
- stats.byDay[date] = { date, cost: 0, tokens: 0 };
857
+ if (!stats.byTime[timeKey]) {
858
+ stats.byTime[timeKey] = { date: timeKey, cost: 0, tokens: 0, inputTokens: 0, outputTokens: 0 };
859
+ }
860
+ stats.byTime[timeKey].cost += cost;
861
+ stats.byTime[timeKey].tokens += tokens;
862
+ stats.byTime[timeKey].inputTokens += inputTokens;
863
+ stats.byTime[timeKey].outputTokens += outputTokens;
864
+
865
+ const projectName = projectInfo.name;
866
+ if (!stats.byProject[projectId]) {
867
+ stats.byProject[projectId] = { id: projectId, name: projectName, cost: 0, tokens: 0, inputTokens: 0, outputTokens: 0 };
775
868
  }
776
- stats.byDay[date].cost += cost;
777
- stats.byDay[date].tokens += tokens;
869
+ stats.byProject[projectId].cost += cost;
870
+ stats.byProject[projectId].tokens += tokens;
871
+ stats.byProject[projectId].inputTokens += inputTokens;
872
+ stats.byProject[projectId].outputTokens += outputTokens;
778
873
  }
779
874
  }
780
875
  } catch (err) {
@@ -792,7 +887,7 @@ app.get('/api/usage', async (req, res) => {
792
887
  const messages = fs.readdirSync(sessionDir);
793
888
  for (const msgFile of messages) {
794
889
  if (msgFile.endsWith('.json')) {
795
- processMessage(path.join(sessionDir, msgFile));
890
+ processMessage(path.join(sessionDir, msgFile), session);
796
891
  }
797
892
  }
798
893
  }
@@ -806,7 +901,8 @@ app.get('/api/usage', async (req, res) => {
806
901
  totalCost: stats.totalCost,
807
902
  totalTokens: stats.totalTokens,
808
903
  byModel: Object.values(stats.byModel).sort((a, b) => b.cost - a.cost),
809
- byDay: Object.values(stats.byDay).sort((a, b) => a.date.localeCompare(b.date))
904
+ byDay: Object.values(stats.byTime).sort((a, b) => a.date.localeCompare(b.date)),
905
+ byProject: Object.values(stats.byProject).sort((a, b) => b.cost - a.cost)
810
906
  };
811
907
 
812
908
  res.json(response);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-studio-server",
3
- "version": "1.0.13",
3
+ "version": "1.1.2",
4
4
  "description": "Backend server for OpenCode Studio - manages opencode configurations",
5
5
  "main": "index.js",
6
6
  "bin": {