@openchamber/web 1.4.2 → 1.4.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.
@@ -29,6 +29,22 @@ const normalizeDirectoryPath = (value) => {
29
29
  return trimmed;
30
30
  };
31
31
 
32
+ const cleanBranchName = (branch) => {
33
+ if (!branch) {
34
+ return branch;
35
+ }
36
+ if (branch.startsWith('refs/heads/')) {
37
+ return branch.substring('refs/heads/'.length);
38
+ }
39
+ if (branch.startsWith('heads/')) {
40
+ return branch.substring('heads/'.length);
41
+ }
42
+ if (branch.startsWith('refs/')) {
43
+ return branch.substring('refs/'.length);
44
+ }
45
+ return branch;
46
+ };
47
+
32
48
  export async function isGitRepository(directory) {
33
49
  const directoryPath = normalizeDirectoryPath(directory);
34
50
  if (!directoryPath || !fs.existsSync(directoryPath)) {
@@ -805,7 +821,7 @@ export async function getWorktrees(directory) {
805
821
  } else if (line.startsWith('HEAD ')) {
806
822
  current.head = line.substring(5);
807
823
  } else if (line.startsWith('branch ')) {
808
- current.branch = line.substring(7);
824
+ current.branch = cleanBranchName(line.substring(7));
809
825
  } else if (line === '') {
810
826
  if (current.worktree) {
811
827
  worktrees.push(current);
@@ -1086,3 +1102,16 @@ export async function getCommitFiles(directory, commitHash) {
1086
1102
  throw error;
1087
1103
  }
1088
1104
  }
1105
+
1106
+ export async function renameBranch(directory, oldName, newName) {
1107
+ const git = simpleGit(normalizeDirectoryPath(directory));
1108
+
1109
+ try {
1110
+ // Use git branch -m command to rename the branch
1111
+ await git.raw(['branch', '-m', oldName, newName]);
1112
+ return { success: true, branch: newName };
1113
+ } catch (error) {
1114
+ console.error('Failed to rename branch:', error);
1115
+ throw error;
1116
+ }
1117
+ }
@@ -101,22 +101,72 @@ function getAgentWritePath(agentName, workingDirectory, requestedScope) {
101
101
  if (existing.path) {
102
102
  return existing;
103
103
  }
104
-
104
+
105
105
  // For new agents or built-in overrides: use requested scope or default to user
106
106
  const scope = requestedScope || AGENT_SCOPE.USER;
107
107
  if (scope === AGENT_SCOPE.PROJECT && workingDirectory) {
108
- return {
109
- scope: AGENT_SCOPE.PROJECT,
110
- path: getProjectAgentPath(workingDirectory, agentName)
108
+ return {
109
+ scope: AGENT_SCOPE.PROJECT,
110
+ path: getProjectAgentPath(workingDirectory, agentName)
111
111
  };
112
112
  }
113
-
114
- return {
115
- scope: AGENT_SCOPE.USER,
116
- path: getUserAgentPath(agentName)
113
+
114
+ return {
115
+ scope: AGENT_SCOPE.USER,
116
+ path: getUserAgentPath(agentName)
117
117
  };
118
118
  }
119
119
 
120
+ /**
121
+ * Detect where an agent's permission field is currently defined
122
+ * Priority: project .md > user .md > project JSON > user JSON
123
+ * Returns: { source: 'md'|'json'|null, scope: 'project'|'user'|null, path: string|null }
124
+ */
125
+ function getAgentPermissionSource(agentName, workingDirectory) {
126
+ // Check project-level .md first
127
+ if (workingDirectory) {
128
+ const projectMdPath = getProjectAgentPath(workingDirectory, agentName);
129
+ if (fs.existsSync(projectMdPath)) {
130
+ const { frontmatter } = parseMdFile(projectMdPath);
131
+ if (frontmatter.permission !== undefined) {
132
+ return { source: 'md', scope: AGENT_SCOPE.PROJECT, path: projectMdPath };
133
+ }
134
+ }
135
+ }
136
+
137
+ // Check user-level .md
138
+ const userMdPath = getUserAgentPath(agentName);
139
+ if (fs.existsSync(userMdPath)) {
140
+ const { frontmatter } = parseMdFile(userMdPath);
141
+ if (frontmatter.permission !== undefined) {
142
+ return { source: 'md', scope: AGENT_SCOPE.USER, path: userMdPath };
143
+ }
144
+ }
145
+
146
+ // Check JSON layers (project > user)
147
+ const layers = readConfigLayers(workingDirectory);
148
+
149
+ // Project opencode.json
150
+ const projectJsonPermission = layers.projectConfig?.agent?.[agentName]?.permission;
151
+ if (projectJsonPermission !== undefined && layers.paths.projectPath) {
152
+ return { source: 'json', scope: AGENT_SCOPE.PROJECT, path: layers.paths.projectPath };
153
+ }
154
+
155
+ // User opencode.json
156
+ const userJsonPermission = layers.userConfig?.agent?.[agentName]?.permission;
157
+ if (userJsonPermission !== undefined) {
158
+ return { source: 'json', scope: AGENT_SCOPE.USER, path: layers.paths.userPath };
159
+ }
160
+
161
+ // Custom config (env var)
162
+ const customJsonPermission = layers.customConfig?.agent?.[agentName]?.permission;
163
+ if (customJsonPermission !== undefined && layers.paths.customPath) {
164
+ return { source: 'json', scope: 'custom', path: layers.paths.customPath };
165
+ }
166
+
167
+ return { source: null, scope: null, path: null };
168
+ }
169
+
120
170
  // ============== COMMAND SCOPE HELPERS ==============
121
171
 
122
172
  /**
@@ -412,9 +462,125 @@ function writePromptFile(filePath, content) {
412
462
  console.log(`Updated prompt file: ${filePath}`);
413
463
  }
414
464
 
465
+ /**
466
+ * Get all possible project config paths in priority order
467
+ * Priority: root > .opencode/, json > jsonc
468
+ */
469
+ function getProjectConfigCandidates(workingDirectory) {
470
+ if (!workingDirectory) return [];
471
+ return [
472
+ path.join(workingDirectory, 'opencode.json'),
473
+ path.join(workingDirectory, 'opencode.jsonc'),
474
+ path.join(workingDirectory, '.opencode', 'opencode.json'),
475
+ path.join(workingDirectory, '.opencode', 'opencode.jsonc'),
476
+ ];
477
+ }
478
+
479
+ /**
480
+ * Find existing project config file or return default path for new config
481
+ */
415
482
  function getProjectConfigPath(workingDirectory) {
416
483
  if (!workingDirectory) return null;
417
- return path.join(workingDirectory, 'opencode.json');
484
+
485
+ const candidates = getProjectConfigCandidates(workingDirectory);
486
+
487
+ // Return first existing config file
488
+ for (const candidate of candidates) {
489
+ if (fs.existsSync(candidate)) {
490
+ return candidate;
491
+ }
492
+ }
493
+
494
+ // Default to root opencode.json for new configs
495
+ return candidates[0];
496
+ }
497
+
498
+ /**
499
+ * Merge new permission config with existing non-wildcard patterns
500
+ * Non-wildcard patterns (patterns other than "*") are preserved from existing config
501
+ * @param {object|string|null} newPermission - New permission config from UI (wildcards only)
502
+ * @param {object} permissionSource - Result from getAgentPermissionSource
503
+ * @param {string} agentName - Agent name
504
+ * @param {string|null} workingDirectory - Working directory
505
+ * @returns {object|string|null} Merged permission config
506
+ */
507
+ function mergePermissionWithNonWildcards(newPermission, permissionSource, agentName, workingDirectory) {
508
+ // If no existing permission, return new permission as-is
509
+ if (!permissionSource.source || !permissionSource.path) {
510
+ return newPermission;
511
+ }
512
+
513
+ // Get existing permission config
514
+ let existingPermission = null;
515
+ if (permissionSource.source === 'md') {
516
+ const { frontmatter } = parseMdFile(permissionSource.path);
517
+ existingPermission = frontmatter.permission;
518
+ } else if (permissionSource.source === 'json') {
519
+ const config = readConfigFile(permissionSource.path);
520
+ existingPermission = config?.agent?.[agentName]?.permission;
521
+ }
522
+
523
+ // If no existing permission or it's a simple string, return new permission as-is
524
+ if (!existingPermission || typeof existingPermission === 'string') {
525
+ return newPermission;
526
+ }
527
+
528
+ // If new permission is null/undefined, return null to clear it
529
+ if (newPermission == null) {
530
+ return null;
531
+ }
532
+
533
+ // If new permission is a simple string (e.g., "allow"), return it as-is
534
+ if (typeof newPermission === 'string') {
535
+ return newPermission;
536
+ }
537
+
538
+ // Extract non-wildcard patterns from existing permission
539
+ const nonWildcardPatterns = {};
540
+ for (const [permKey, permValue] of Object.entries(existingPermission)) {
541
+ if (permKey === '*') continue; // Skip global default
542
+
543
+ if (typeof permValue === 'object' && permValue !== null && !Array.isArray(permValue)) {
544
+ // Permission has pattern-based config (e.g., { "npm *": "allow", "*": "ask" })
545
+ const nonWildcards = {};
546
+ for (const [pattern, action] of Object.entries(permValue)) {
547
+ if (pattern !== '*') {
548
+ nonWildcards[pattern] = action;
549
+ }
550
+ }
551
+ if (Object.keys(nonWildcards).length > 0) {
552
+ nonWildcardPatterns[permKey] = nonWildcards;
553
+ }
554
+ }
555
+ // Simple string values (e.g., "allow") don't have patterns, skip them
556
+ }
557
+
558
+ // If no non-wildcard patterns to preserve, return new permission as-is
559
+ if (Object.keys(nonWildcardPatterns).length === 0) {
560
+ return newPermission;
561
+ }
562
+
563
+ // Merge non-wildcards into new permission
564
+ const merged = { ...newPermission };
565
+ for (const [permKey, patterns] of Object.entries(nonWildcardPatterns)) {
566
+ const newValue = merged[permKey];
567
+ if (typeof newValue === 'string') {
568
+ // Convert string to object with wildcard + preserved patterns
569
+ merged[permKey] = { '*': newValue, ...patterns };
570
+ } else if (typeof newValue === 'object' && newValue !== null) {
571
+ // Merge patterns, new wildcards take precedence
572
+ merged[permKey] = { ...patterns, ...newValue };
573
+ } else {
574
+ // Permission not in new config - preserve existing patterns with their wildcard if it existed
575
+ const existingValue = existingPermission[permKey];
576
+ if (typeof existingValue === 'object' && existingValue !== null) {
577
+ const wildcard = existingValue['*'];
578
+ merged[permKey] = wildcard ? { '*': wildcard, ...patterns } : patterns;
579
+ }
580
+ }
581
+ }
582
+
583
+ return merged;
418
584
  }
419
585
 
420
586
  function getConfigPaths(workingDirectory) {
@@ -539,22 +705,23 @@ function getJsonWriteTarget(layers, preferredScope) {
539
705
  }
540
706
 
541
707
  function parseMdFile(filePath) {
542
- try {
543
- const content = fs.readFileSync(filePath, 'utf8');
544
- const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n([\s\S]*)$/);
545
-
546
- if (!match) {
547
- return { frontmatter: {}, body: content.trim() };
548
- }
708
+ const content = fs.readFileSync(filePath, 'utf8');
709
+ const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n([\s\S]*)$/);
549
710
 
550
- const frontmatter = yaml.parse(match[1]) || {};
551
- const body = match[2].trim();
711
+ if (!match) {
712
+ return { frontmatter: {}, body: content.trim() };
713
+ }
552
714
 
553
- return { frontmatter, body };
715
+ let frontmatter = {};
716
+ try {
717
+ frontmatter = yaml.parse(match[1]) || {};
554
718
  } catch (error) {
555
- console.error(`Failed to parse markdown file ${filePath}:`, error);
556
- throw new Error('Failed to parse agent markdown file');
719
+ console.warn(`Failed to parse markdown frontmatter ${filePath}, treating as empty:`, error);
720
+ frontmatter = {};
557
721
  }
722
+
723
+ const body = match[2].trim();
724
+ return { frontmatter, body };
558
725
  }
559
726
 
560
727
  function writeMdFile(filePath, frontmatter, body) {
@@ -606,12 +773,6 @@ function getAgentSources(agentName, workingDirectory) {
606
773
  scope: jsonSource.exists ? jsonScope : null,
607
774
  fields: []
608
775
  },
609
- json: {
610
- exists: jsonSource.exists,
611
- path: jsonPath,
612
- scope: jsonSource.exists ? jsonScope : null,
613
- fields: []
614
- },
615
776
  // Additional info about both levels
616
777
  projectMd: {
617
778
  exists: projectExists,
@@ -638,6 +799,48 @@ function getAgentSources(agentName, workingDirectory) {
638
799
  return sources;
639
800
  }
640
801
 
802
+ function getAgentConfig(agentName, workingDirectory) {
803
+ // Prefer markdown agents (project > user)
804
+ const projectPath = workingDirectory ? getProjectAgentPath(workingDirectory, agentName) : null;
805
+ const projectExists = projectPath && fs.existsSync(projectPath);
806
+
807
+ const userPath = getUserAgentPath(agentName);
808
+ const userExists = fs.existsSync(userPath);
809
+
810
+ if (projectExists || userExists) {
811
+ const mdPath = projectExists ? projectPath : userPath;
812
+ const { frontmatter, body } = parseMdFile(mdPath);
813
+
814
+ return {
815
+ source: 'md',
816
+ scope: projectExists ? AGENT_SCOPE.PROJECT : AGENT_SCOPE.USER,
817
+ config: {
818
+ ...frontmatter,
819
+ ...(typeof body === 'string' && body.length > 0 ? { prompt: body } : {}),
820
+ },
821
+ };
822
+ }
823
+
824
+ // Then fall back to opencode.json (highest-precedence entry)
825
+ const layers = readConfigLayers(workingDirectory);
826
+ const jsonSource = getJsonEntrySource(layers, 'agent', agentName);
827
+
828
+ if (jsonSource.exists && jsonSource.section) {
829
+ const scope = jsonSource.path === layers.paths.projectPath ? AGENT_SCOPE.PROJECT : AGENT_SCOPE.USER;
830
+ return {
831
+ source: 'json',
832
+ scope,
833
+ config: { ...jsonSource.section },
834
+ };
835
+ }
836
+
837
+ return {
838
+ source: 'none',
839
+ scope: null,
840
+ config: {},
841
+ };
842
+ }
843
+
641
844
  function createAgent(agentName, config, workingDirectory, scope) {
642
845
  ensureDirs();
643
846
 
@@ -693,7 +896,7 @@ function updateAgent(agentName, updates, workingDirectory) {
693
896
  const hasJsonFields = jsonSource.exists && jsonSection && Object.keys(jsonSection).length > 0;
694
897
  const jsonTarget = jsonSource.exists
695
898
  ? { config: jsonSource.config, path: jsonSource.path }
696
- : getJsonWriteTarget(layers, workingDirectory ? AGENT_SCOPE.PROJECT : AGENT_SCOPE.USER);
899
+ : getJsonWriteTarget(layers, AGENT_SCOPE.USER);
697
900
  let config = jsonTarget.config || {};
698
901
 
699
902
  // Determine if we should create a new md file:
@@ -742,7 +945,7 @@ function updateAgent(agentName, updates, workingDirectory) {
742
945
  jsonModified = true;
743
946
  continue;
744
947
  }
745
-
948
+
746
949
  // For JSON-only agents, store prompt inline in JSON
747
950
  if (!config.agent) config.agent = {};
748
951
  if (!config.agent[agentName]) config.agent[agentName] = {};
@@ -751,9 +954,79 @@ function updateAgent(agentName, updates, workingDirectory) {
751
954
  continue;
752
955
  }
753
956
 
957
+ // Special handling for permission field - uses location detection and preserves non-wildcards
958
+ if (field === 'permission') {
959
+ const permissionSource = getAgentPermissionSource(agentName, workingDirectory);
960
+ const newPermission = mergePermissionWithNonWildcards(value, permissionSource, agentName, workingDirectory);
961
+
962
+ if (permissionSource.source === 'md') {
963
+ // Write to existing .md file
964
+ const existingMdData = parseMdFile(permissionSource.path);
965
+ existingMdData.frontmatter.permission = newPermission;
966
+ writeMdFile(permissionSource.path, existingMdData.frontmatter, existingMdData.body);
967
+ console.log(`Updated permission in .md file: ${permissionSource.path}`);
968
+ } else if (permissionSource.source === 'json') {
969
+ // Write to existing JSON location
970
+ const existingConfig = readConfigFile(permissionSource.path);
971
+ if (!existingConfig.agent) existingConfig.agent = {};
972
+ if (!existingConfig.agent[agentName]) existingConfig.agent[agentName] = {};
973
+ existingConfig.agent[agentName].permission = newPermission;
974
+ writeConfig(existingConfig, permissionSource.path);
975
+ console.log(`Updated permission in JSON: ${permissionSource.path}`);
976
+ } else {
977
+ // Permission not defined anywhere - use agent's source location
978
+ if ((mdExists || creatingNewMd) && mdData) {
979
+ mdData.frontmatter.permission = newPermission;
980
+ mdModified = true;
981
+ } else if (hasJsonFields) {
982
+ // Agent exists in JSON - add permission there
983
+ if (!config.agent) config.agent = {};
984
+ if (!config.agent[agentName]) config.agent[agentName] = {};
985
+ config.agent[agentName].permission = newPermission;
986
+ jsonModified = true;
987
+ } else {
988
+ // Built-in agent with no config - write to project JSON if available, else user JSON
989
+ const writeTarget = workingDirectory
990
+ ? { config: layers.projectConfig || {}, path: layers.paths.projectPath || layers.paths.userPath }
991
+ : { config: layers.userConfig || {}, path: layers.paths.userPath };
992
+ if (!writeTarget.config.agent) writeTarget.config.agent = {};
993
+ if (!writeTarget.config.agent[agentName]) writeTarget.config.agent[agentName] = {};
994
+ writeTarget.config.agent[agentName].permission = newPermission;
995
+ writeConfig(writeTarget.config, writeTarget.path);
996
+ console.log(`Created permission in JSON: ${writeTarget.path}`);
997
+ }
998
+ }
999
+ continue;
1000
+ }
1001
+
754
1002
  const inMd = mdData?.frontmatter?.[field] !== undefined;
755
1003
  const inJson = jsonSection?.[field] !== undefined;
756
1004
 
1005
+ if (value === null) {
1006
+ // Treat null as a request to remove the field.
1007
+ if (mdData && inMd) {
1008
+ delete mdData.frontmatter[field];
1009
+ mdModified = true;
1010
+ }
1011
+
1012
+ if (inJson) {
1013
+ if (config.agent?.[agentName]) {
1014
+ delete config.agent[agentName][field];
1015
+
1016
+ if (Object.keys(config.agent[agentName]).length === 0) {
1017
+ delete config.agent[agentName];
1018
+ }
1019
+ if (Object.keys(config.agent).length === 0) {
1020
+ delete config.agent;
1021
+ }
1022
+
1023
+ jsonModified = true;
1024
+ }
1025
+ }
1026
+
1027
+ continue;
1028
+ }
1029
+
757
1030
  // JSON takes precedence over md, so update JSON first if field exists there
758
1031
  if (inJson) {
759
1032
  if (!config.agent) config.agent = {};
@@ -852,6 +1125,7 @@ function getCommandSources(commandName, workingDirectory) {
852
1125
  const jsonSource = getJsonEntrySource(layers, 'command', commandName);
853
1126
  const jsonSection = jsonSource.section;
854
1127
  const jsonPath = jsonSource.path || layers.paths.customPath || layers.paths.projectPath || layers.paths.userPath;
1128
+ const jsonScope = jsonSource.path === layers.paths.projectPath ? COMMAND_SCOPE.PROJECT : COMMAND_SCOPE.USER;
855
1129
 
856
1130
  const sources = {
857
1131
  md: {
@@ -863,6 +1137,7 @@ function getCommandSources(commandName, workingDirectory) {
863
1137
  json: {
864
1138
  exists: jsonSource.exists,
865
1139
  path: jsonPath,
1140
+ scope: jsonSource.exists ? jsonScope : null,
866
1141
  fields: []
867
1142
  },
868
1143
  // Additional info about both levels
@@ -1373,6 +1648,8 @@ function deleteSkill(skillName, workingDirectory) {
1373
1648
  export {
1374
1649
  getAgentSources,
1375
1650
  getAgentScope,
1651
+ getAgentPermissionSource,
1652
+ getAgentConfig,
1376
1653
  createAgent,
1377
1654
  updateAgent,
1378
1655
  deleteAgent,