@openchamber/web 1.7.0 → 1.7.1

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/index.html CHANGED
@@ -203,10 +203,10 @@
203
203
  pointer-events: none;
204
204
  }
205
205
  </style>
206
- <script type="module" crossorigin src="/assets/index-hNdAvOl1.js"></script>
206
+ <script type="module" crossorigin src="/assets/index-CHfHzhF4.js"></script>
207
207
  <link rel="modulepreload" crossorigin href="/assets/vendor-.bun-Dd1DA83-.js">
208
208
  <link rel="stylesheet" crossorigin href="/assets/vendor--DbVqbJpV.css">
209
- <link rel="stylesheet" crossorigin href="/assets/index-DWAk1auj.css">
209
+ <link rel="stylesheet" crossorigin href="/assets/index-9lSEGPDM.css">
210
210
  </head>
211
211
  <body class="h-full bg-background text-foreground">
212
212
  <div id="root" class="h-full">
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openchamber/web",
3
- "version": "1.7.0",
3
+ "version": "1.7.1",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "main": "./server/index.js",
package/server/index.js CHANGED
@@ -6893,6 +6893,133 @@ async function main(options = {}) {
6893
6893
  SKILL_DIR,
6894
6894
  } = await import('./lib/opencode-config.js');
6895
6895
 
6896
+ const findWorktreeRootForSkills = (workingDirectory) => {
6897
+ if (!workingDirectory) return null;
6898
+ let current = path.resolve(workingDirectory);
6899
+ while (true) {
6900
+ if (fs.existsSync(path.join(current, '.git'))) {
6901
+ return current;
6902
+ }
6903
+ const parent = path.dirname(current);
6904
+ if (parent === current) {
6905
+ return null;
6906
+ }
6907
+ current = parent;
6908
+ }
6909
+ };
6910
+
6911
+ const getSkillProjectAncestors = (workingDirectory) => {
6912
+ if (!workingDirectory) return [];
6913
+ const result = [];
6914
+ let current = path.resolve(workingDirectory);
6915
+ const stop = findWorktreeRootForSkills(workingDirectory) || current;
6916
+ while (true) {
6917
+ result.push(current);
6918
+ if (current === stop) break;
6919
+ const parent = path.dirname(current);
6920
+ if (parent === current) break;
6921
+ current = parent;
6922
+ }
6923
+ return result;
6924
+ };
6925
+
6926
+ const isPathInside = (candidatePath, parentPath) => {
6927
+ if (!candidatePath || !parentPath) return false;
6928
+ const normalizedCandidate = path.resolve(candidatePath);
6929
+ const normalizedParent = path.resolve(parentPath);
6930
+ return normalizedCandidate === normalizedParent || normalizedCandidate.startsWith(`${normalizedParent}${path.sep}`);
6931
+ };
6932
+
6933
+ const inferSkillScopeAndSourceFromPath = (skillPath, workingDirectory) => {
6934
+ const resolvedPath = typeof skillPath === 'string' ? path.resolve(skillPath) : '';
6935
+ const home = os.homedir();
6936
+ const source = resolvedPath.includes(`${path.sep}.agents${path.sep}skills${path.sep}`)
6937
+ ? 'agents'
6938
+ : resolvedPath.includes(`${path.sep}.claude${path.sep}skills${path.sep}`)
6939
+ ? 'claude'
6940
+ : 'opencode';
6941
+
6942
+ const projectAncestors = getSkillProjectAncestors(workingDirectory);
6943
+ const isProjectScoped = projectAncestors.some((ancestor) => {
6944
+ const candidates = [
6945
+ path.join(ancestor, '.opencode'),
6946
+ path.join(ancestor, '.claude', 'skills'),
6947
+ path.join(ancestor, '.agents', 'skills'),
6948
+ ];
6949
+ return candidates.some((candidate) => isPathInside(resolvedPath, candidate));
6950
+ });
6951
+
6952
+ if (isProjectScoped) {
6953
+ return { scope: SKILL_SCOPE.PROJECT, source };
6954
+ }
6955
+
6956
+ const userRoots = [
6957
+ path.join(home, '.config', 'opencode'),
6958
+ path.join(home, '.opencode'),
6959
+ path.join(home, '.claude', 'skills'),
6960
+ path.join(home, '.agents', 'skills'),
6961
+ process.env.OPENCODE_CONFIG_DIR ? path.resolve(process.env.OPENCODE_CONFIG_DIR) : null,
6962
+ ].filter(Boolean);
6963
+
6964
+ if (userRoots.some((root) => isPathInside(resolvedPath, root))) {
6965
+ return { scope: SKILL_SCOPE.USER, source };
6966
+ }
6967
+
6968
+ return { scope: SKILL_SCOPE.USER, source };
6969
+ };
6970
+
6971
+ const fetchOpenCodeDiscoveredSkills = async (workingDirectory) => {
6972
+ if (!openCodePort) {
6973
+ return null;
6974
+ }
6975
+
6976
+ try {
6977
+ const url = new URL(buildOpenCodeUrl('/skill', ''));
6978
+ if (workingDirectory) {
6979
+ url.searchParams.set('directory', workingDirectory);
6980
+ }
6981
+
6982
+ const response = await fetch(url.toString(), {
6983
+ method: 'GET',
6984
+ headers: {
6985
+ Accept: 'application/json',
6986
+ ...getOpenCodeAuthHeaders(),
6987
+ },
6988
+ signal: AbortSignal.timeout(8_000),
6989
+ });
6990
+
6991
+ if (!response.ok) {
6992
+ return null;
6993
+ }
6994
+
6995
+ const payload = await response.json();
6996
+ if (!Array.isArray(payload)) {
6997
+ return null;
6998
+ }
6999
+
7000
+ return payload
7001
+ .map((item) => {
7002
+ const name = typeof item?.name === 'string' ? item.name.trim() : '';
7003
+ const location = typeof item?.location === 'string' ? item.location : '';
7004
+ const description = typeof item?.description === 'string' ? item.description : '';
7005
+ if (!name || !location) {
7006
+ return null;
7007
+ }
7008
+ const inferred = inferSkillScopeAndSourceFromPath(location, workingDirectory);
7009
+ return {
7010
+ name,
7011
+ path: location,
7012
+ scope: inferred.scope,
7013
+ source: inferred.source,
7014
+ description,
7015
+ };
7016
+ })
7017
+ .filter(Boolean);
7018
+ } catch {
7019
+ return null;
7020
+ }
7021
+ };
7022
+
6896
7023
  // List all discovered skills
6897
7024
  app.get('/api/config/skills', async (req, res) => {
6898
7025
  try {
@@ -6900,11 +7027,11 @@ async function main(options = {}) {
6900
7027
  if (!directory) {
6901
7028
  return res.status(400).json({ error });
6902
7029
  }
6903
- const skills = discoverSkills(directory);
7030
+ const skills = (await fetchOpenCodeDiscoveredSkills(directory)) || discoverSkills(directory);
6904
7031
 
6905
7032
  // Enrich with full sources info
6906
7033
  const enrichedSkills = skills.map(skill => {
6907
- const sources = getSkillSources(skill.name, directory);
7034
+ const sources = getSkillSources(skill.name, directory, skill);
6908
7035
  return {
6909
7036
  ...skill,
6910
7037
  sources
@@ -7018,7 +7145,9 @@ async function main(options = {}) {
7018
7145
  return res.status(404).json({ ok: false, error: { kind: 'invalidSource', message: 'Unknown source' } });
7019
7146
  }
7020
7147
 
7021
- const discovered = directory ? discoverSkills(directory) : [];
7148
+ const discovered = directory
7149
+ ? ((await fetchOpenCodeDiscoveredSkills(directory)) || discoverSkills(directory))
7150
+ : [];
7022
7151
  const installedByName = new Map(discovered.map((s) => [s.name, s]));
7023
7152
 
7024
7153
  if (src.sourceType === 'clawdhub' || isClawdHubSource(src.source)) {
@@ -7033,7 +7162,7 @@ async function main(options = {}) {
7033
7162
  ...item,
7034
7163
  sourceId: src.id,
7035
7164
  installed: installed
7036
- ? { isInstalled: true, scope: installed.scope }
7165
+ ? { isInstalled: true, scope: installed.scope, source: installed.source }
7037
7166
  : { isInstalled: false },
7038
7167
  };
7039
7168
  });
@@ -7077,7 +7206,7 @@ async function main(options = {}) {
7077
7206
  ...item,
7078
7207
  gitIdentityId: src.gitIdentityId,
7079
7208
  installed: installed
7080
- ? { isInstalled: true, scope: installed.scope }
7209
+ ? { isInstalled: true, scope: installed.scope, source: installed.source }
7081
7210
  : { isInstalled: false },
7082
7211
  };
7083
7212
  });
@@ -7131,6 +7260,7 @@ async function main(options = {}) {
7131
7260
  subpath,
7132
7261
  gitIdentityId,
7133
7262
  scope,
7263
+ targetSource,
7134
7264
  selections,
7135
7265
  conflictPolicy,
7136
7266
  conflictDecisions,
@@ -7152,6 +7282,7 @@ async function main(options = {}) {
7152
7282
  if (isClawdHubSource(source)) {
7153
7283
  const result = await installSkillsFromClawdHub({
7154
7284
  scope,
7285
+ targetSource,
7155
7286
  workingDirectory,
7156
7287
  userSkillDir: SKILL_DIR,
7157
7288
  selections,
@@ -7166,7 +7297,22 @@ async function main(options = {}) {
7166
7297
  return res.status(400).json({ ok: false, error: result.error });
7167
7298
  }
7168
7299
 
7169
- return res.json({ ok: true, installed: result.installed || [], skipped: result.skipped || [] });
7300
+ const installed = result.installed || [];
7301
+ const skipped = result.skipped || [];
7302
+ const requiresReload = installed.length > 0;
7303
+
7304
+ if (requiresReload) {
7305
+ await refreshOpenCodeAfterConfigChange('skills install');
7306
+ }
7307
+
7308
+ return res.json({
7309
+ ok: true,
7310
+ installed,
7311
+ skipped,
7312
+ requiresReload,
7313
+ message: requiresReload ? 'Skills installed successfully. Reloading interface…' : 'No skills were installed',
7314
+ reloadDelayMs: requiresReload ? CLIENT_RELOAD_DELAY_MS : undefined,
7315
+ });
7170
7316
  }
7171
7317
 
7172
7318
  // Handle GitHub sources (git clone based)
@@ -7177,6 +7323,7 @@ async function main(options = {}) {
7177
7323
  subpath,
7178
7324
  identity,
7179
7325
  scope,
7326
+ targetSource,
7180
7327
  workingDirectory,
7181
7328
  userSkillDir: SKILL_DIR,
7182
7329
  selections,
@@ -7202,7 +7349,22 @@ async function main(options = {}) {
7202
7349
  return res.status(400).json({ ok: false, error: result.error });
7203
7350
  }
7204
7351
 
7205
- res.json({ ok: true, installed: result.installed || [], skipped: result.skipped || [] });
7352
+ const installed = result.installed || [];
7353
+ const skipped = result.skipped || [];
7354
+ const requiresReload = installed.length > 0;
7355
+
7356
+ if (requiresReload) {
7357
+ await refreshOpenCodeAfterConfigChange('skills install');
7358
+ }
7359
+
7360
+ res.json({
7361
+ ok: true,
7362
+ installed,
7363
+ skipped,
7364
+ requiresReload,
7365
+ message: requiresReload ? 'Skills installed successfully. Reloading interface…' : 'No skills were installed',
7366
+ reloadDelayMs: requiresReload ? CLIENT_RELOAD_DELAY_MS : undefined,
7367
+ });
7206
7368
  } catch (error) {
7207
7369
  console.error('Failed to install skills:', error);
7208
7370
  res.status(500).json({ ok: false, error: { kind: 'unknown', message: error.message || 'Failed to install skills' } });
@@ -7217,7 +7379,9 @@ async function main(options = {}) {
7217
7379
  if (!directory) {
7218
7380
  return res.status(400).json({ error });
7219
7381
  }
7220
- const sources = getSkillSources(skillName, directory);
7382
+ const discoveredSkill = ((await fetchOpenCodeDiscoveredSkills(directory)) || [])
7383
+ .find((skill) => skill.name === skillName) || null;
7384
+ const sources = getSkillSources(skillName, directory, discoveredSkill);
7221
7385
 
7222
7386
  res.json({
7223
7387
  name: skillName,
@@ -7242,7 +7406,9 @@ async function main(options = {}) {
7242
7406
  return res.status(400).json({ error });
7243
7407
  }
7244
7408
 
7245
- const sources = getSkillSources(skillName, directory);
7409
+ const discoveredSkill = ((await fetchOpenCodeDiscoveredSkills(directory)) || [])
7410
+ .find((skill) => skill.name === skillName) || null;
7411
+ const sources = getSkillSources(skillName, directory, discoveredSkill);
7246
7412
  if (!sources.md.exists || !sources.md.dir) {
7247
7413
  return res.status(404).json({ error: 'Skill not found' });
7248
7414
  }
@@ -7263,7 +7429,7 @@ async function main(options = {}) {
7263
7429
  app.post('/api/config/skills/:name', async (req, res) => {
7264
7430
  try {
7265
7431
  const skillName = req.params.name;
7266
- const { scope, ...config } = req.body;
7432
+ const { scope, source: skillSource, ...config } = req.body;
7267
7433
  const { directory, error } = await resolveProjectDirectory(req);
7268
7434
  if (!directory) {
7269
7435
  return res.status(400).json({ error });
@@ -7272,13 +7438,14 @@ async function main(options = {}) {
7272
7438
  console.log('[Server] Creating skill:', skillName);
7273
7439
  console.log('[Server] Scope:', scope, 'Working directory:', directory);
7274
7440
 
7275
- createSkill(skillName, config, directory, scope);
7276
- // Skills are just files - OpenCode loads them on-demand, no restart needed
7441
+ createSkill(skillName, { ...config, source: skillSource }, directory, scope);
7442
+ await refreshOpenCodeAfterConfigChange('skill creation');
7277
7443
 
7278
7444
  res.json({
7279
7445
  success: true,
7280
- requiresReload: false,
7281
- message: `Skill ${skillName} created successfully`,
7446
+ requiresReload: true,
7447
+ message: `Skill ${skillName} created successfully. Reloading interface…`,
7448
+ reloadDelayMs: CLIENT_RELOAD_DELAY_MS,
7282
7449
  });
7283
7450
  } catch (error) {
7284
7451
  console.error('Failed to create skill:', error);
@@ -7300,12 +7467,13 @@ async function main(options = {}) {
7300
7467
  console.log('[Server] Working directory:', directory);
7301
7468
 
7302
7469
  updateSkill(skillName, updates, directory);
7303
- // Skills are just files - OpenCode loads them on-demand, no restart needed
7470
+ await refreshOpenCodeAfterConfigChange('skill update');
7304
7471
 
7305
7472
  res.json({
7306
7473
  success: true,
7307
- requiresReload: false,
7308
- message: `Skill ${skillName} updated successfully`,
7474
+ requiresReload: true,
7475
+ message: `Skill ${skillName} updated successfully. Reloading interface…`,
7476
+ reloadDelayMs: CLIENT_RELOAD_DELAY_MS,
7309
7477
  });
7310
7478
  } catch (error) {
7311
7479
  console.error('[Server] Failed to update skill:', error);
@@ -7324,7 +7492,9 @@ async function main(options = {}) {
7324
7492
  return res.status(400).json({ error });
7325
7493
  }
7326
7494
 
7327
- const sources = getSkillSources(skillName, directory);
7495
+ const discoveredSkill = ((await fetchOpenCodeDiscoveredSkills(directory)) || [])
7496
+ .find((skill) => skill.name === skillName) || null;
7497
+ const sources = getSkillSources(skillName, directory, discoveredSkill);
7328
7498
  if (!sources.md.exists || !sources.md.dir) {
7329
7499
  return res.status(404).json({ error: 'Skill not found' });
7330
7500
  }
@@ -7351,7 +7521,9 @@ async function main(options = {}) {
7351
7521
  return res.status(400).json({ error });
7352
7522
  }
7353
7523
 
7354
- const sources = getSkillSources(skillName, directory);
7524
+ const discoveredSkill = ((await fetchOpenCodeDiscoveredSkills(directory)) || [])
7525
+ .find((skill) => skill.name === skillName) || null;
7526
+ const sources = getSkillSources(skillName, directory, discoveredSkill);
7355
7527
  if (!sources.md.exists || !sources.md.dir) {
7356
7528
  return res.status(404).json({ error: 'Skill not found' });
7357
7529
  }
@@ -7378,12 +7550,13 @@ async function main(options = {}) {
7378
7550
  }
7379
7551
 
7380
7552
  deleteSkill(skillName, directory);
7381
- // Skills are just files - OpenCode loads them on-demand, no restart needed
7553
+ await refreshOpenCodeAfterConfigChange('skill deletion');
7382
7554
 
7383
7555
  res.json({
7384
7556
  success: true,
7385
- requiresReload: false,
7386
- message: `Skill ${skillName} deleted successfully`,
7557
+ requiresReload: true,
7558
+ message: `Skill ${skillName} deleted successfully. Reloading interface…`,
7559
+ reloadDelayMs: CLIENT_RELOAD_DELAY_MS,
7387
7560
  });
7388
7561
  } catch (error) {
7389
7562
  console.error('Failed to delete skill:', error);