coding-tool-x 3.6.12 → 3.7.0

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 (80) hide show
  1. package/README.md +46 -33
  2. package/dist/web/assets/{Analytics-BuBlsbcI.js → Analytics-Dma_j2Rq.js} +1 -1
  3. package/dist/web/assets/{ConfigTemplates-DQczxuOX.js → ConfigTemplates-C49Y1PLH.js} +1 -1
  4. package/dist/web/assets/Home-DGGC8H4g.js +1 -0
  5. package/dist/web/assets/Home-ewgeIhF7.css +1 -0
  6. package/dist/web/assets/{PluginManager-C8gAlaps.js → PluginManager-B1ECTHyY.js} +1 -1
  7. package/dist/web/assets/{ProjectList-DL4JK6ci.css → ProjectList-B_L55-l9.css} +1 -1
  8. package/dist/web/assets/ProjectList-DyiGyT-H.js +1 -0
  9. package/dist/web/assets/SessionList-CH5GwwiJ.js +1 -0
  10. package/dist/web/assets/{SkillManager-BDbNbNkK.js → SkillManager-CRyVhjYQ.js} +1 -1
  11. package/dist/web/assets/{WorkspaceManager-DGGaGW2c.js → WorkspaceManager-CO9tclVt.js} +1 -1
  12. package/dist/web/assets/{icons-QFXzEgqM.js → icons-Dop5j7Bf.js} +1 -1
  13. package/dist/web/assets/index-CM26UrnJ.css +1 -0
  14. package/dist/web/assets/index-EQq56pRa.js +2 -0
  15. package/dist/web/assets/{naive-ui-kcWDeJwl.js → naive-ui-CVyvdmg1.js} +1 -1
  16. package/dist/web/index.html +4 -4
  17. package/package.json +2 -1
  18. package/src/commands/channels.js +85 -24
  19. package/src/commands/daemon.js +7 -4
  20. package/src/commands/doctor.js +17 -8
  21. package/src/commands/logs.js +48 -6
  22. package/src/commands/port-config.js +81 -75
  23. package/src/commands/proxy-control.js +52 -19
  24. package/src/commands/stats.js +37 -19
  25. package/src/commands/toggle-proxy.js +131 -53
  26. package/src/commands/ui.js +3 -4
  27. package/src/config/default.js +14 -3
  28. package/src/config/paths.js +99 -11
  29. package/src/index.js +11 -16
  30. package/src/server/api/commands.js +1 -1
  31. package/src/server/api/config-export.js +4 -2
  32. package/src/server/api/config-registry.js +40 -34
  33. package/src/server/api/config.js +2 -1
  34. package/src/server/api/dashboard.js +22 -6
  35. package/src/server/api/mcp.js +5 -3
  36. package/src/server/api/pi-channels.js +349 -0
  37. package/src/server/api/pi-config.js +164 -0
  38. package/src/server/api/pi-projects.js +76 -0
  39. package/src/server/api/pi-proxy.js +154 -0
  40. package/src/server/api/pi-sessions.js +334 -0
  41. package/src/server/api/pi-statistics.js +40 -0
  42. package/src/server/api/plugins.js +1 -1
  43. package/src/server/api/prompts.js +3 -2
  44. package/src/server/api/skills.js +1 -1
  45. package/src/server/api/workspaces.js +2 -2
  46. package/src/server/index.js +41 -0
  47. package/src/server/pi-proxy-server.js +66 -0
  48. package/src/server/services/agents-service.js +2 -1
  49. package/src/server/services/base/proxy-utils.js +1 -0
  50. package/src/server/services/channel-balance.js +151 -1
  51. package/src/server/services/channel-scheduler.js +7 -1
  52. package/src/server/services/commands-service.js +11 -2
  53. package/src/server/services/config-export-service.js +275 -18
  54. package/src/server/services/config-registry-service.js +20 -18
  55. package/src/server/services/config-sync-manager.js +115 -6
  56. package/src/server/services/config-sync-service.js +4 -2
  57. package/src/server/services/config-templates-service.js +183 -45
  58. package/src/server/services/mcp-service.js +217 -33
  59. package/src/server/services/pi-auth-providers.js +294 -0
  60. package/src/server/services/pi-channels.js +94 -0
  61. package/src/server/services/pi-config.js +287 -0
  62. package/src/server/services/pi-sessions.js +490 -0
  63. package/src/server/services/pi-settings-manager.js +571 -0
  64. package/src/server/services/pi-statistics-service.js +317 -0
  65. package/src/server/services/plugins-service.js +673 -11
  66. package/src/server/services/prompts-service.js +112 -7
  67. package/src/server/services/proxy-log-helper.js +1 -0
  68. package/src/server/services/proxy-runtime.js +5 -1
  69. package/src/server/services/skill-service.js +13 -3
  70. package/src/server/services/speed-test.js +9 -3
  71. package/src/server/services/ui-config.js +50 -12
  72. package/src/server/services/workspace-service.js +19 -5
  73. package/src/server/websocket-server.js +7 -1
  74. package/dist/web/assets/Home-BvAzzWnB.js +0 -1
  75. package/dist/web/assets/Home-CIua-b-o.css +0 -1
  76. package/dist/web/assets/ProjectList-nqgi6_cR.js +0 -1
  77. package/dist/web/assets/SessionList-D8AxMYy6.js +0 -1
  78. package/dist/web/assets/index-Dsh4Q5Cg.css +0 -1
  79. package/dist/web/assets/index-y-eEayxR.js +0 -2
  80. package/src/utils/cli-flags.js +0 -7
@@ -12,12 +12,14 @@ const path = require('path');
12
12
  const { execFileSync } = require('child_process');
13
13
  const toml = require('toml');
14
14
  const tomlStringify = require('@iarna/toml').stringify;
15
+ const yaml = require('js-yaml');
15
16
  const AdmZip = require('adm-zip');
16
17
  const { listPlugins, getPlugin, updatePlugin: updatePluginRegistry } = require('../../plugins/registry');
17
18
  const { installPlugin: installPluginCore, uninstallPlugin: uninstallPluginCore } = require('../../plugins/plugin-installer');
18
19
  const { initializePlugins, shutdownPlugins } = require('../../plugins/plugin-manager');
19
20
  const { INSTALLED_DIR, CONFIG_DIR } = require('../../plugins/constants');
20
21
  const { NATIVE_PATHS, PATHS } = require('../../config/paths');
22
+ const { getPiCommand } = require('./pi-config');
21
23
  const { maskToken } = require('./oauth-utils');
22
24
  const {
23
25
  assertInsideAllowedRoots,
@@ -26,7 +28,8 @@ const {
26
28
  resolveInsideRoot
27
29
  } = require('./config-artifact-paths');
28
30
 
29
- const CLAUDE_PLUGINS_DIR = path.join(path.dirname(NATIVE_PATHS.claude.settings), 'plugins');
31
+ const CLAUDE_PLUGINS_DIR = NATIVE_PATHS.claude.plugins
32
+ || path.join(NATIVE_PATHS.claude.dir || path.dirname(NATIVE_PATHS.claude.settings), 'plugins');
30
33
  const CLAUDE_INSTALLED_FILE = path.join(CLAUDE_PLUGINS_DIR, 'installed_plugins.json');
31
34
  const CLAUDE_MARKETPLACES_FILE = path.join(CLAUDE_PLUGINS_DIR, 'known_marketplaces.json');
32
35
  const CLAUDE_PLUGINS_CACHE_DIR = path.join(CLAUDE_PLUGINS_DIR, 'cache');
@@ -42,9 +45,10 @@ const DEFAULT_REPOS_BY_PLATFORM = {
42
45
  claude: [],
43
46
  codex: [],
44
47
  gemini: [],
45
- opencode: []
48
+ opencode: [],
49
+ pi: []
46
50
  };
47
- const SUPPORTED_PLATFORMS = ['claude', 'codex', 'gemini', 'opencode'];
51
+ const SUPPORTED_PLATFORMS = ['claude', 'codex', 'gemini', 'opencode', 'pi'];
48
52
  const PLATFORM_CAPABILITIES = {
49
53
  claude: {
50
54
  platform: 'claude',
@@ -94,6 +98,19 @@ const PLATFORM_CAPABILITIES = {
94
98
  config: true,
95
99
  import: false,
96
100
  syncRepos: false
101
+ },
102
+ pi: {
103
+ platform: 'pi',
104
+ supportsPlugins: true,
105
+ repositories: true,
106
+ market: true,
107
+ install: true,
108
+ uninstall: true,
109
+ toggle: true,
110
+ config: true,
111
+ import: false,
112
+ syncRepos: false,
113
+ pluginKindLabel: 'packages/extensions'
97
114
  }
98
115
  };
99
116
 
@@ -177,6 +194,134 @@ function splitPluginMarketplaceKey(key = '') {
177
194
  };
178
195
  }
179
196
 
197
+ function normalizePiPackageEntry(item = null) {
198
+ if (typeof item === 'string') {
199
+ const name = item.trim();
200
+ return name ? {
201
+ name,
202
+ source: name,
203
+ directory: name,
204
+ installSource: name,
205
+ resourceTypes: []
206
+ } : null;
207
+ }
208
+ if (!item || typeof item !== 'object') return null;
209
+
210
+ const rawName = item.name || item.package || item.id || item.source || item.path || '';
211
+ const name = String(rawName || '').trim();
212
+ if (!name) return null;
213
+
214
+ const directory = String(item.directory || item.path || name).trim() || name;
215
+ const installSource = String(item.installSource || item.source || item.url || item.packageSource || name).trim() || name;
216
+ return {
217
+ ...item,
218
+ name,
219
+ source: item.source || installSource,
220
+ directory,
221
+ installSource,
222
+ resourceTypes: normalizePiResourceTypes(item.resourceTypes || item.resources || item.provides || item.capabilities)
223
+ };
224
+ }
225
+
226
+ function getPiPackageIdentity(item = null) {
227
+ const normalized = normalizePiPackageEntry(item);
228
+ return normalized?.name || '';
229
+ }
230
+
231
+ function piPackageSettingValue(item = null) {
232
+ const normalized = normalizePiPackageEntry(item);
233
+ if (!normalized) return null;
234
+ if (typeof item === 'string') return normalized.name;
235
+
236
+ const next = { ...item };
237
+ next.name = normalized.name;
238
+ next.directory = normalized.directory;
239
+ next.source = normalized.installSource;
240
+ const resourceTypes = normalizePiResourceTypes(next.resourceTypes || next.resources || normalized.resourceTypes);
241
+ if (resourceTypes.length > 0) {
242
+ next.resourceTypes = resourceTypes;
243
+ } else {
244
+ delete next.resourceTypes;
245
+ }
246
+ return next;
247
+ }
248
+
249
+ function normalizePiResourceType(value = '') {
250
+ const normalized = String(value || '').trim();
251
+ if (!normalized) return '';
252
+ const key = normalized
253
+ .replace(/[\s_-]+/g, '')
254
+ .toLowerCase();
255
+ const map = {
256
+ extension: 'extension',
257
+ extensions: 'extension',
258
+ plugin: 'extension',
259
+ plugins: 'extension',
260
+ skill: 'skill',
261
+ skills: 'skill',
262
+ prompt: 'promptTemplate',
263
+ prompts: 'promptTemplate',
264
+ prompttemplate: 'promptTemplate',
265
+ prompttemplates: 'promptTemplate',
266
+ prompttemplating: 'promptTemplate',
267
+ command: 'promptTemplate',
268
+ commands: 'promptTemplate',
269
+ theme: 'theme',
270
+ themes: 'theme',
271
+ mcp: 'mcp',
272
+ subagent: 'subagent',
273
+ subagents: 'subagent',
274
+ agent: 'subagent',
275
+ agents: 'subagent'
276
+ };
277
+ return map[key] || normalized;
278
+ }
279
+
280
+ function normalizePiResourceTypes(input = []) {
281
+ const values = [];
282
+ const pushValue = (value) => {
283
+ const normalized = normalizePiResourceType(value);
284
+ if (normalized && !values.includes(normalized)) values.push(normalized);
285
+ };
286
+
287
+ if (Array.isArray(input)) {
288
+ input.forEach(pushValue);
289
+ } else if (input && typeof input === 'object') {
290
+ for (const [key, value] of Object.entries(input)) {
291
+ if (value === false || value == null) continue;
292
+ if (Array.isArray(value)) {
293
+ if (value.length > 0) pushValue(key);
294
+ } else if (typeof value === 'object') {
295
+ if (Object.keys(value).length > 0) pushValue(key);
296
+ } else {
297
+ pushValue(key);
298
+ }
299
+ }
300
+ } else if (input) {
301
+ pushValue(input);
302
+ }
303
+
304
+ return values;
305
+ }
306
+
307
+ function isPiPackageInstallSource(value = '') {
308
+ const source = String(value || '').trim();
309
+ if (!source) return false;
310
+ if (isLikelyLocalPath(source)) return true;
311
+ if (/^(npm|git):/i.test(source)) return true;
312
+ if (/^https?:\/\//i.test(source)) return true;
313
+ if (/^ssh:\/\//i.test(source)) return true;
314
+ if (/^git@[^:]+:.+/i.test(source)) return true;
315
+ return !source.startsWith('-');
316
+ }
317
+
318
+ function normalizeMarketplaceSkillPaths(skills = []) {
319
+ if (!Array.isArray(skills)) return [];
320
+ return skills
321
+ .map(skill => normalizeRepoPath(String(skill || '').replace(/^\.\//, '')))
322
+ .filter(Boolean);
323
+ }
324
+
180
325
  function hasRepoInstallInfo(repoInfo = null) {
181
326
  return Boolean(
182
327
  repoInfo &&
@@ -544,11 +689,14 @@ class PluginsService {
544
689
  plugin.repoId || '',
545
690
  plugin.repoProvider || '',
546
691
  plugin.repoOwner || '',
547
- plugin.repoName || '',
548
- plugin.repoProjectPath || '',
549
- plugin.repoLocalPath || '',
550
- plugin.directory || plugin.installSource || ''
551
- ].join('::');
692
+ plugin.repoName || '',
693
+ plugin.repoProjectPath || '',
694
+ plugin.repoLocalPath || '',
695
+ plugin.directory || '',
696
+ plugin.installSource || '',
697
+ plugin.pluginKind || '',
698
+ plugin.marketplaceFormat || ''
699
+ ].join('::');
552
700
  if (seen.has(key)) continue;
553
701
  seen.add(key);
554
702
  deduped.push({
@@ -571,6 +719,10 @@ class PluginsService {
571
719
  return this.platform === 'opencode';
572
720
  }
573
721
 
722
+ _isPi() {
723
+ return this.platform === 'pi';
724
+ }
725
+
574
726
  _isCodex() {
575
727
  return this.platform === 'codex';
576
728
  }
@@ -596,6 +748,194 @@ class PluginsService {
596
748
  return json;
597
749
  }
598
750
 
751
+ _readPiSettings() {
752
+ const filePath = NATIVE_PATHS.pi.settings;
753
+ if (!fs.existsSync(filePath)) {
754
+ const legacyPath = NATIVE_PATHS.pi.settingsJsonLegacy;
755
+ if (legacyPath && fs.existsSync(legacyPath)) {
756
+ try {
757
+ const raw = fs.readFileSync(legacyPath, 'utf8');
758
+ return raw.trim() ? JSON.parse(raw) : {};
759
+ } catch (err) {
760
+ console.error('[PluginsService] Failed to read legacy OMP settings:', err.message);
761
+ }
762
+ }
763
+ return {};
764
+ }
765
+
766
+ try {
767
+ const raw = fs.readFileSync(filePath, 'utf8');
768
+ const parsed = raw.trim() ? yaml.load(raw) : {};
769
+ return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {};
770
+ } catch (err) {
771
+ console.error('[PluginsService] Failed to read OMP settings:', err.message);
772
+ return {};
773
+ }
774
+ }
775
+
776
+ _writePiSettings(settings = {}) {
777
+ this._ensureDir(path.dirname(NATIVE_PATHS.pi.settings));
778
+ fs.writeFileSync(NATIVE_PATHS.pi.settings, yaml.dump(settings, {
779
+ lineWidth: 120,
780
+ noRefs: true,
781
+ sortKeys: false
782
+ }), 'utf8');
783
+ }
784
+
785
+ _listPiPackages() {
786
+ const settings = this._readPiSettings();
787
+ return Array.isArray(settings.packages)
788
+ ? settings.packages.map(normalizePiPackageEntry).filter(Boolean)
789
+ : [];
790
+ }
791
+
792
+ _setPiPackages(packages = []) {
793
+ const settings = this._readPiSettings();
794
+ const seen = new Set();
795
+ settings.packages = packages
796
+ .map(item => piPackageSettingValue(item))
797
+ .filter((item) => {
798
+ const name = getPiPackageIdentity(item);
799
+ if (!name || seen.has(name)) return false;
800
+ seen.add(name);
801
+ return true;
802
+ });
803
+ this._writePiSettings(settings);
804
+ }
805
+
806
+ _listPiDisabledPackageEntries() {
807
+ const settings = this._readPiSettings();
808
+ return Array.isArray(settings.disabledPackages)
809
+ ? settings.disabledPackages.map(normalizePiPackageEntry).filter(Boolean)
810
+ : [];
811
+ }
812
+
813
+ _listPiDisabledPackages() {
814
+ return this._listPiDisabledPackageEntries().map(item => item.name).filter(Boolean);
815
+ }
816
+
817
+ _setPiDisabledPackages(packages = []) {
818
+ const settings = this._readPiSettings();
819
+ const seen = new Set();
820
+ settings.disabledPackages = packages
821
+ .map(item => piPackageSettingValue(item))
822
+ .filter((item) => {
823
+ const name = getPiPackageIdentity(item);
824
+ if (!name || seen.has(name)) return false;
825
+ seen.add(name);
826
+ return true;
827
+ });
828
+ this._writePiSettings(settings);
829
+ }
830
+
831
+ _runPiPackageCommand(args = []) {
832
+ execFileSync(getPiCommand(), args, {
833
+ cwd: process.cwd(),
834
+ encoding: 'utf8',
835
+ stdio: 'pipe',
836
+ timeout: 120000
837
+ });
838
+ }
839
+
840
+ _registerPiPackageReference(packageName, metadata = {}) {
841
+ const packages = this._listPiPackages();
842
+ const installSource = String(metadata.installSource || metadata.source || packageName || '').trim();
843
+ const displayName = String(metadata.name || packageName || installSource).trim();
844
+ if (!packages.some(pkg => pkg.name === displayName || pkg.installSource === installSource)) {
845
+ const resourceTypes = normalizePiResourceTypes(metadata.resourceTypes || metadata.resources);
846
+ const entry = installSource && installSource !== displayName
847
+ ? {
848
+ name: displayName,
849
+ source: installSource,
850
+ ...(metadata.version ? { version: metadata.version } : {}),
851
+ ...(metadata.description ? { description: metadata.description } : {}),
852
+ ...(resourceTypes.length > 0 ? { resourceTypes } : {})
853
+ }
854
+ : displayName;
855
+ packages.push(entry);
856
+ this._setPiPackages(packages);
857
+ }
858
+ }
859
+
860
+ _buildPiPackagePlugin(packageName, metadata = {}) {
861
+ const installSource = String(metadata.installSource || metadata.source || packageName || '').trim();
862
+ const name = String(metadata.name || packageName || installSource).trim();
863
+ const resourceTypes = normalizePiResourceTypes(metadata.resourceTypes || metadata.resources);
864
+ return {
865
+ name,
866
+ directory: name,
867
+ installSource: installSource || name,
868
+ version: metadata.version || 'latest',
869
+ description: metadata.description || '',
870
+ pluginKind: 'package',
871
+ pluginType: 'package',
872
+ ...(resourceTypes.length > 0 ? { resourceTypes } : {})
873
+ };
874
+ }
875
+
876
+ _installPiPackage(packageName, metadata = {}) {
877
+ const installSource = String(metadata.installSource || metadata.source || packageName || '').trim();
878
+ const plugin = this._buildPiPackagePlugin(packageName, { ...metadata, installSource });
879
+ try {
880
+ this._runPiPackageCommand(['install', installSource || packageName]);
881
+ return {
882
+ success: true,
883
+ plugin
884
+ };
885
+ } catch (err) {
886
+ this._registerPiPackageReference(packageName, { ...metadata, installSource, name: plugin.name });
887
+ return {
888
+ success: true,
889
+ registeredOnly: true,
890
+ warning: `Failed to run "${getPiCommand()} install"; saved the package reference so OMP can resolve it later. ${err.message}`,
891
+ plugin
892
+ };
893
+ }
894
+ }
895
+
896
+ _removePiPackage(packageName) {
897
+ try {
898
+ this._runPiPackageCommand(['remove', packageName]);
899
+ return true;
900
+ } catch (err) {
901
+ return false;
902
+ }
903
+ }
904
+
905
+ _listPiLocalExtensions() {
906
+ const plugins = [];
907
+ const extensionsDir = NATIVE_PATHS.pi.extensions;
908
+ if (!fs.existsSync(extensionsDir)) return plugins;
909
+
910
+ try {
911
+ const disabled = new Set(this._listPiDisabledPackages());
912
+ const entries = fs.readdirSync(extensionsDir, { withFileTypes: true });
913
+ for (const entry of entries) {
914
+ if (entry.name.startsWith('.')) continue;
915
+ const fullPath = path.join(extensionsDir, entry.name);
916
+ const ext = path.extname(entry.name).toLowerCase();
917
+ if (!entry.isDirectory() && !['.js', '.mjs', '.cjs', '.ts'].includes(ext)) continue;
918
+ const name = entry.isDirectory() ? entry.name : entry.name.slice(0, -ext.length);
919
+ plugins.push({
920
+ name,
921
+ directory: entry.name,
922
+ installPath: fullPath,
923
+ source: 'pi-extension',
924
+ version: 'local',
925
+ description: '',
926
+ installed: true,
927
+ enabled: !disabled.has(name) && !disabled.has(entry.name),
928
+ pluginType: entry.isDirectory() ? 'extension-directory' : 'extension-file',
929
+ pluginKind: 'extension'
930
+ });
931
+ }
932
+ } catch (err) {
933
+ console.error('[PluginsService] Failed to list OMP extensions:', err.message);
934
+ }
935
+
936
+ return plugins;
937
+ }
938
+
599
939
  _readOpenCodeConfig() {
600
940
  const filePath = this._getOpenCodeConfigPath();
601
941
  if (!fs.existsSync(filePath)) return { filePath, config: {} };
@@ -1112,6 +1452,41 @@ class PluginsService {
1112
1452
  return { plugins };
1113
1453
  }
1114
1454
 
1455
+ if (this._isPi()) {
1456
+ const disabledEntries = this._listPiDisabledPackageEntries();
1457
+ const disabled = new Set(disabledEntries.flatMap(item => [item.name, item.installSource].filter(Boolean)));
1458
+ const plugins = [];
1459
+ const seen = new Set();
1460
+
1461
+ for (const pkg of this._listPiPackages()) {
1462
+ const seenKey = pkg.installSource || pkg.name;
1463
+ if (seen.has(seenKey)) continue;
1464
+ seen.add(seenKey);
1465
+ plugins.push({
1466
+ ...pkg,
1467
+ name: pkg.name,
1468
+ directory: pkg.directory || pkg.name,
1469
+ installSource: pkg.installSource || pkg.source || pkg.name,
1470
+ resourceTypes: normalizePiResourceTypes(pkg.resourceTypes || pkg.resources),
1471
+ source: 'pi-settings',
1472
+ version: pkg.version || 'latest',
1473
+ description: pkg.description || '',
1474
+ installed: true,
1475
+ enabled: !disabled.has(pkg.name) && !disabled.has(pkg.installSource),
1476
+ pluginType: 'package',
1477
+ pluginKind: 'package'
1478
+ });
1479
+ }
1480
+
1481
+ for (const extension of this._listPiLocalExtensions()) {
1482
+ if (seen.has(extension.name)) continue;
1483
+ seen.add(extension.name);
1484
+ plugins.push(extension);
1485
+ }
1486
+
1487
+ return { plugins };
1488
+ }
1489
+
1115
1490
  const plugins = [];
1116
1491
 
1117
1492
  // Read Claude Code's installed_plugins.json
@@ -1251,6 +1626,12 @@ class PluginsService {
1251
1626
  return plugin;
1252
1627
  }
1253
1628
 
1629
+ if (this._isPi()) {
1630
+ const plugin = this.listPlugins().plugins.find(p => p.name === name || p.directory === name);
1631
+ if (!plugin) return null;
1632
+ return plugin;
1633
+ }
1634
+
1254
1635
  const plugin = getPlugin(name);
1255
1636
  if (!plugin) {
1256
1637
  return null;
@@ -1314,6 +1695,51 @@ class PluginsService {
1314
1695
  };
1315
1696
  }
1316
1697
 
1698
+ if (this._isPi()) {
1699
+ const packageMetadata = repoInfo && typeof repoInfo === 'object'
1700
+ ? {
1701
+ name: repoInfo.name || repoInfo.packageName || repoInfo.displayName,
1702
+ version: repoInfo.version,
1703
+ description: repoInfo.description,
1704
+ resourceTypes: repoInfo.resourceTypes || repoInfo.resources,
1705
+ installSource: repoInfo.installSource || repoInfo.source
1706
+ }
1707
+ : {};
1708
+ const explicitPackageSource = String(source || packageMetadata.installSource || '').trim();
1709
+
1710
+ if (
1711
+ repoInfo?.pluginKind === 'package' ||
1712
+ repoInfo?.pluginType === 'package' ||
1713
+ (explicitPackageSource && isPiPackageInstallSource(explicitPackageSource) && !hasRepoInstallInfo(repoInfo))
1714
+ ) {
1715
+ return this._installPiPackage(explicitPackageSource, packageMetadata);
1716
+ }
1717
+
1718
+ if (hasRepoInstallInfo(repoInfo)) {
1719
+ return this._installFromRepoDirectory(repoInfo, { installRoot: NATIVE_PATHS.pi.extensions });
1720
+ }
1721
+
1722
+ const parsedSource = this.parseRepoTreeSource(source);
1723
+ if (parsedSource) {
1724
+ return this._installPiPackage(source);
1725
+ }
1726
+
1727
+ const parsedRepo = this._repoFromGitUrl(source, 'main');
1728
+ if (parsedRepo) {
1729
+ return this._installPiPackage(source);
1730
+ }
1731
+
1732
+ const packageName = String(source || '').trim();
1733
+ if (packageName && isPiPackageInstallSource(packageName)) {
1734
+ return this._installPiPackage(packageName);
1735
+ }
1736
+
1737
+ return {
1738
+ success: false,
1739
+ error: 'OMP plugin install expects package name, repository metadata, a GitHub/GitLab tree URL, or a Git repository URL'
1740
+ };
1741
+ }
1742
+
1317
1743
  if (this._isOpenCode()) {
1318
1744
  if (hasRepoInstallInfo(repoInfo)) {
1319
1745
  return this._installFromRepoDirectory(repoInfo, { installRoot: this._getOpenCodePluginsDir() });
@@ -1552,7 +1978,7 @@ class PluginsService {
1552
1978
  } catch (e) {
1553
1979
  console.error('[PluginsService] Failed to update native installed_plugins.json:', e.message);
1554
1980
  }
1555
- } else if (!this._isOpenCode()) {
1981
+ } else if (!this._isOpenCode() && !this._isPi()) {
1556
1982
  const installTimestamp = new Date().toISOString();
1557
1983
  const sourceUrl = this.buildRepoBrowserUrl(normalizedRepo, directory) || buildRepoUrl(normalizedRepo);
1558
1984
  const repoSourceMeta = {
@@ -1582,6 +2008,21 @@ class PluginsService {
1582
2008
  }
1583
2009
 
1584
2010
  this.writeRepoSourceMeta(pluginDir, repoSourceMeta);
2011
+ } else if (this._isPi()) {
2012
+ const sourceUrl = this.buildRepoBrowserUrl(normalizedRepo, directory) || buildRepoUrl(normalizedRepo);
2013
+ this.writeRepoSourceMeta(pluginDir, {
2014
+ repoId: normalizedRepo.id,
2015
+ repoProvider: normalizedRepo.provider,
2016
+ repoOwner: normalizedRepo.owner || '',
2017
+ repoName: normalizedRepo.name || '',
2018
+ repoBranch: normalizedRepo.branch,
2019
+ repoDirectory: directory,
2020
+ repoHost: normalizedRepo.host || '',
2021
+ repoProjectPath: normalizedRepo.projectPath || '',
2022
+ repoLocalPath: normalizedRepo.localPath || '',
2023
+ repoUrl: normalizedRepo.repoUrl || buildRepoUrl(normalizedRepo),
2024
+ source: sourceUrl
2025
+ });
1585
2026
  }
1586
2027
 
1587
2028
  return {
@@ -1607,6 +2048,9 @@ class PluginsService {
1607
2048
  if (this._isOpenCode()) {
1608
2049
  return ['package.json', 'plugin.json'];
1609
2050
  }
2051
+ if (this._isPi()) {
2052
+ return ['pi.json', 'extension.json', 'plugin.json', 'package.json'];
2053
+ }
1610
2054
  return ['.claude-plugin/plugin.json', 'plugin.json', 'package.json'];
1611
2055
  }
1612
2056
 
@@ -1738,6 +2182,71 @@ class PluginsService {
1738
2182
  };
1739
2183
  }
1740
2184
 
2185
+ if (this._isPi()) {
2186
+ const targetName = String(name || '').trim();
2187
+ let safeName = '';
2188
+ try {
2189
+ safeName = normalizePluginPathName(targetName, 'plugin name');
2190
+ } catch {
2191
+ safeName = '';
2192
+ }
2193
+ const packages = this._listPiPackages();
2194
+ let removed = false;
2195
+ const packageToRemove = packages.find(pkg =>
2196
+ pkg.name === targetName ||
2197
+ pkg.name === safeName ||
2198
+ pkg.installSource === targetName ||
2199
+ pkg.installSource === safeName
2200
+ );
2201
+ if (packageToRemove) {
2202
+ this._removePiPackage(packageToRemove.installSource || packageToRemove.name);
2203
+ }
2204
+ const nextPackages = packages.filter(pkg =>
2205
+ pkg.name !== safeName &&
2206
+ pkg.name !== targetName &&
2207
+ pkg.installSource !== safeName &&
2208
+ pkg.installSource !== targetName
2209
+ );
2210
+ if (nextPackages.length !== packages.length) {
2211
+ this._setPiPackages(nextPackages);
2212
+ const disabledEntries = this._listPiDisabledPackageEntries().filter(pkg =>
2213
+ pkg.name !== safeName &&
2214
+ pkg.name !== targetName &&
2215
+ pkg.installSource !== safeName &&
2216
+ pkg.installSource !== targetName &&
2217
+ pkg.name !== packageToRemove?.name &&
2218
+ pkg.installSource !== packageToRemove?.installSource
2219
+ );
2220
+ this._setPiDisabledPackages(disabledEntries);
2221
+ removed = true;
2222
+ }
2223
+
2224
+ const extensionsDir = NATIVE_PATHS.pi.extensions;
2225
+ if (safeName && fs.existsSync(extensionsDir)) {
2226
+ const directPath = resolveInsideRoot(extensionsDir, safeName, 'OMP extension path');
2227
+ if (fs.existsSync(directPath)) {
2228
+ fs.rmSync(directPath, { recursive: true, force: true });
2229
+ removed = true;
2230
+ } else {
2231
+ const entries = fs.readdirSync(extensionsDir, { withFileTypes: true });
2232
+ for (const entry of entries) {
2233
+ const baseName = entry.name.replace(path.extname(entry.name), '');
2234
+ if (entry.name === safeName || baseName === safeName) {
2235
+ const entryPath = resolveInsideRoot(extensionsDir, entry.name, 'OMP extension path');
2236
+ fs.rmSync(entryPath, { recursive: true, force: true });
2237
+ removed = true;
2238
+ break;
2239
+ }
2240
+ }
2241
+ }
2242
+ }
2243
+
2244
+ return {
2245
+ success: true,
2246
+ message: removed ? 'Plugin removed successfully' : 'Plugin not found'
2247
+ };
2248
+ }
2249
+
1741
2250
  if (this._isOpenCode()) {
1742
2251
  const safeName = normalizePluginPathName(name, 'plugin name');
1743
2252
  const pluginsDir = this._getOpenCodePluginsDir();
@@ -1870,6 +2379,29 @@ class PluginsService {
1870
2379
  throw new Error(`${this.platform} plugin management is not supported`);
1871
2380
  }
1872
2381
 
2382
+ if (this._isPi()) {
2383
+ const targetName = String(name || '').trim();
2384
+ const packages = this._listPiPackages();
2385
+ const targetPackage = packages.find(pkg => pkg.name === targetName || pkg.installSource === targetName);
2386
+ const disabledEntries = this._listPiDisabledPackageEntries();
2387
+ const disabled = new Set(disabledEntries.flatMap(item => [item.name, item.installSource].filter(Boolean)));
2388
+ if (enabled) {
2389
+ disabled.delete(targetName);
2390
+ if (targetPackage) {
2391
+ disabled.delete(targetPackage.name);
2392
+ disabled.delete(targetPackage.installSource);
2393
+ }
2394
+ } else {
2395
+ disabled.add(targetPackage || targetName);
2396
+ }
2397
+ this._setPiDisabledPackages(Array.from(disabled));
2398
+ return {
2399
+ name: targetName,
2400
+ enabled,
2401
+ success: true
2402
+ };
2403
+ }
2404
+
1873
2405
  if (this._isOpenCode()) {
1874
2406
  const configured = this._listOpenCodeConfiguredPlugins();
1875
2407
  const exists = configured.includes(name);
@@ -1963,6 +2495,19 @@ class PluginsService {
1963
2495
  throw new Error(`${this.platform} plugin management is not supported`);
1964
2496
  }
1965
2497
 
2498
+ if (this._isPi()) {
2499
+ const settings = this._readPiSettings();
2500
+ settings.packageConfig = settings.packageConfig && typeof settings.packageConfig === 'object' && !Array.isArray(settings.packageConfig)
2501
+ ? settings.packageConfig
2502
+ : {};
2503
+ settings.packageConfig[name] = config;
2504
+ this._writePiSettings(settings);
2505
+ return {
2506
+ success: true,
2507
+ message: `Configuration updated for plugin "${name}"`
2508
+ };
2509
+ }
2510
+
1966
2511
  if (this._isOpenCode()) {
1967
2512
  const configDir = path.join(OPENCODE_CONFIG_DIR, 'plugins-config');
1968
2513
  const configFile = resolvePluginConfigFile(configDir, name);
@@ -2798,6 +3343,13 @@ class PluginsService {
2798
3343
  lspServers: data.lspServers || null,
2799
3344
  commands: data.commands || [],
2800
3345
  hooks: data.hooks || [],
3346
+ resourceTypes: normalizePiResourceTypes(data.resourceTypes || data.resources),
3347
+ resources: data.resources || null,
3348
+ containsSkills: Boolean(data.containsSkills),
3349
+ skillPaths: Array.isArray(data.skillPaths) ? data.skillPaths : [],
3350
+ pluginKind: data.pluginKind || (data.containsSkills ? 'skill-bundle' : 'plugin'),
3351
+ pluginType: data.pluginType || '',
3352
+ strict: data.strict,
2801
3353
  isInstalled: false
2802
3354
  };
2803
3355
  }
@@ -2982,6 +3534,7 @@ class PluginsService {
2982
3534
  const results = [];
2983
3535
  for (const plugin of marketplace.plugins) {
2984
3536
  const sourceInfo = this._repoFromMarketplaceSource(repo, plugin.source);
3537
+ const skillPaths = normalizeMarketplaceSkillPaths(plugin.skills);
2985
3538
  const canReadManifest = sourceInfo.repo.provider === 'local' || sourceInfo.repo.id === repo.id;
2986
3539
  const manifest = canReadManifest
2987
3540
  ? await this._readManifestFromRepo(sourceInfo.repo, sourceInfo.directory, [
@@ -3003,7 +3556,11 @@ class PluginsService {
3003
3556
  marketplaceFormat: 'claude-marketplace',
3004
3557
  lspServers: plugin.lspServers || null,
3005
3558
  commands: manifest?.commands || [],
3006
- hooks: manifest?.hooks || []
3559
+ hooks: manifest?.hooks || [],
3560
+ containsSkills: skillPaths.length > 0,
3561
+ skillPaths,
3562
+ pluginKind: skillPaths.length > 0 ? 'skill-bundle' : 'plugin',
3563
+ strict: Object.prototype.hasOwnProperty.call(plugin, 'strict') ? plugin.strict : undefined
3007
3564
  }));
3008
3565
  }
3009
3566
  return results;
@@ -3041,6 +3598,96 @@ class PluginsService {
3041
3598
  return results;
3042
3599
  }
3043
3600
 
3601
+ _normalizePiCatalogEntries(catalog = null) {
3602
+ if (Array.isArray(catalog)) return catalog;
3603
+ if (!catalog || typeof catalog !== 'object') return [];
3604
+ for (const key of ['packages', 'plugins', 'extensions']) {
3605
+ if (Array.isArray(catalog[key])) return catalog[key];
3606
+ }
3607
+ return [];
3608
+ }
3609
+
3610
+ _normalizePiCatalogPackage(repo, entry = {}, catalog = {}, defaults = {}) {
3611
+ if (!entry || typeof entry !== 'object') return null;
3612
+ const installSource = String(
3613
+ entry.installSource ||
3614
+ entry.source ||
3615
+ entry.package ||
3616
+ entry.url ||
3617
+ entry.repo ||
3618
+ entry.repository ||
3619
+ entry.name ||
3620
+ ''
3621
+ ).trim();
3622
+ const name = String(entry.name || entry.id || entry.package || installSource || '').trim();
3623
+ if (!name || !installSource) return null;
3624
+
3625
+ const resourceTypes = normalizePiResourceTypes(
3626
+ entry.resourceTypes ||
3627
+ entry.resources ||
3628
+ entry.provides ||
3629
+ entry.pi?.resources ||
3630
+ defaults.resourceTypes
3631
+ );
3632
+ return this.buildMarketPluginItem(repo, {
3633
+ name,
3634
+ displayName: entry.displayName || entry.title || '',
3635
+ description: entry.description || '',
3636
+ author: entry.author || catalog.author || repo.owner,
3637
+ version: entry.version || 'latest',
3638
+ category: entry.category || 'general',
3639
+ directory: entry.directory || name,
3640
+ marketplace: entry.marketplace || repo.marketplace || catalog.name || defaults.marketplace || '',
3641
+ installSource,
3642
+ marketplaceFormat: defaults.marketplaceFormat || 'pi-package-catalog',
3643
+ pluginKind: 'package',
3644
+ pluginType: 'package',
3645
+ resourceTypes,
3646
+ resources: entry.resources || entry.provides || null,
3647
+ repoUrl: entry.repoUrl || entry.repositoryUrl || entry.repository || repo.repoUrl || buildRepoUrl(repo)
3648
+ });
3649
+ }
3650
+
3651
+ async _fetchPiPackageCatalogPlugins(repo, fileMap, readJson) {
3652
+ if (!this._isPi()) return [];
3653
+ const catalogFiles = ['pi-packages.json', '.pi/packages.json', 'packages.json'];
3654
+ for (const catalogPath of catalogFiles) {
3655
+ if (!fileMap.has(catalogPath)) continue;
3656
+ const catalog = await readJson(catalogPath);
3657
+ const entries = this._normalizePiCatalogEntries(catalog);
3658
+ const plugins = entries
3659
+ .map(entry => this._normalizePiCatalogPackage(repo, entry, catalog, {
3660
+ marketplaceFormat: 'pi-package-catalog'
3661
+ }))
3662
+ .filter(Boolean);
3663
+ if (plugins.length > 0) return plugins;
3664
+ }
3665
+
3666
+ const manifestPath = fileMap.has('pi.json') ? 'pi.json' : (fileMap.has('package.json') ? 'package.json' : '');
3667
+ if (!manifestPath) return [];
3668
+ const manifest = await readJson(manifestPath);
3669
+ const piMeta = manifest.pi && typeof manifest.pi === 'object' ? manifest.pi : {};
3670
+ const installSource = piMeta.installSource || manifest.installSource || manifest.name;
3671
+ const resourceTypes = normalizePiResourceTypes(
3672
+ piMeta.resourceTypes ||
3673
+ piMeta.resources ||
3674
+ manifest.resourceTypes ||
3675
+ manifest.resources
3676
+ );
3677
+ if (!installSource || resourceTypes.length === 0) return [];
3678
+ const plugin = this._normalizePiCatalogPackage(repo, {
3679
+ name: manifest.name || repo.name,
3680
+ installSource,
3681
+ version: manifest.version,
3682
+ description: manifest.description,
3683
+ author: manifest.author,
3684
+ resourceTypes
3685
+ }, { name: repo.marketplace || repo.name }, {
3686
+ marketplaceFormat: manifestPath === 'pi.json' ? 'pi-manifest' : 'pi-package-manifest'
3687
+ });
3688
+ return plugin ? [plugin] : [];
3689
+ }
3690
+
3044
3691
  /**
3045
3692
  * Get market plugins from configured repositories
3046
3693
  * @returns {Promise<Array>} List of available market plugins
@@ -3117,6 +3764,15 @@ class PluginsService {
3117
3764
  }
3118
3765
  }
3119
3766
 
3767
+ // OMP package catalog format; legacy catalog filenames may still include pi.
3768
+ if (this._isPi()) {
3769
+ const piPackagePlugins = await this._fetchPiPackageCatalogPlugins(repo, fileMap, readJson);
3770
+ if (piPackagePlugins.length > 0) {
3771
+ marketPlugins.push(...piPackagePlugins);
3772
+ continue;
3773
+ }
3774
+ }
3775
+
3120
3776
  const nestedManifestFiles = files.filter(file => {
3121
3777
  if (this._isCodex()) {
3122
3778
  return file.path === '.codex-plugin/plugin.json' || file.path.endsWith('/.codex-plugin/plugin.json');
@@ -3162,7 +3818,12 @@ class PluginsService {
3162
3818
 
3163
3819
  for (const dir of pluginDirs) {
3164
3820
  try {
3165
- const manifest = await readJson(`${dir}/plugin.json`);
3821
+ const manifest = this._isPi()
3822
+ ? await this._readManifestFromRepo(repo, dir, ['pi.json', 'extension.json', 'plugin.json', 'package.json'])
3823
+ : await readJson(`${dir}/plugin.json`);
3824
+ if (!manifest) {
3825
+ throw new Error(`File not found: ${dir}/plugin.json`);
3826
+ }
3166
3827
 
3167
3828
  marketPlugins.push(this.buildMarketPluginItem(repo, {
3168
3829
  name: manifest.name || dir,
@@ -3170,6 +3831,7 @@ class PluginsService {
3170
3831
  author: manifest.author || repo.owner,
3171
3832
  version: manifest.version || '1.0.0',
3172
3833
  directory: dir,
3834
+ pluginKind: this._isPi() ? 'extension' : undefined,
3173
3835
  commands: manifest.commands || [],
3174
3836
  hooks: manifest.hooks || []
3175
3837
  }));