shennian 0.3.85 → 0.3.86

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 (99) hide show
  1. package/dist/bin/shennian.js +1 -1
  2. package/dist/publish-build-manifest.json +28 -28
  3. package/dist/src/agents/adapter.js +19 -1
  4. package/dist/src/agents/claude-managed-permissions.js +248 -1
  5. package/dist/src/agents/claude.js +393 -8
  6. package/dist/src/agents/codex-managed-permissions.js +294 -1
  7. package/dist/src/agents/codex-managed-room-tools.js +249 -1
  8. package/dist/src/agents/codex-utils.js +200 -7
  9. package/dist/src/agents/codex.js +1352 -17
  10. package/dist/src/agents/config-status.js +226 -1
  11. package/dist/src/agents/cursor-managed-permissions.js +299 -2
  12. package/dist/src/agents/cursor.js +314 -1
  13. package/dist/src/agents/custom.js +418 -4
  14. package/dist/src/agents/external-channel-instructions.js +94 -11
  15. package/dist/src/agents/gemini.js +180 -1
  16. package/dist/src/agents/manager.d.ts +2 -0
  17. package/dist/src/agents/manager.js +250 -18
  18. package/dist/src/agents/model-registry/cache.js +37 -1
  19. package/dist/src/agents/model-registry/discovery.js +188 -2
  20. package/dist/src/agents/model-registry/parsers.js +521 -4
  21. package/dist/src/agents/model-registry/runner.js +30 -1
  22. package/dist/src/agents/model-registry/service.js +115 -1
  23. package/dist/src/agents/model-registry/types.js +8 -1
  24. package/dist/src/agents/model-registry.js +19 -1
  25. package/dist/src/agents/native-capabilities.js +237 -1
  26. package/dist/src/agents/openclaw.js +293 -2
  27. package/dist/src/agents/opencode-managed-permissions.js +187 -2
  28. package/dist/src/agents/opencode.js +298 -1
  29. package/dist/src/agents/platform-instructions.js +46 -17
  30. package/dist/src/agents/runtime-compatibility.js +44 -1
  31. package/dist/src/agents/workbuddy-managed-permissions.js +329 -2
  32. package/dist/src/agents/workbuddy.js +444 -3
  33. package/dist/src/commands/room.js +2 -2
  34. package/dist/src/fs/boundary.js +185 -1
  35. package/dist/src/fs/handler.js +130 -1
  36. package/dist/src/fs/security.js +32 -1
  37. package/dist/src/fs/text-decoder.js +110 -1
  38. package/dist/src/index.js +587 -3
  39. package/dist/src/manager/prompt.js +34 -29
  40. package/dist/src/manager/registry.js +317 -1
  41. package/dist/src/manager/retired-external-channel-compat.js +40 -1
  42. package/dist/src/manager/runtime.d.ts +4 -0
  43. package/dist/src/manager/runtime.js +1514 -23
  44. package/dist/src/native-fusion/codex-parser.js +660 -4
  45. package/dist/src/native-fusion/codex-user-input.js +102 -5
  46. package/dist/src/native-fusion/config.js +5 -1
  47. package/dist/src/native-fusion/file-watcher.js +191 -1
  48. package/dist/src/native-fusion/local-repository.js +569 -2
  49. package/dist/src/native-fusion/opencode-parser.js +130 -3
  50. package/dist/src/native-fusion/parsers.js +140 -3
  51. package/dist/src/native-fusion/service.js +739 -2
  52. package/dist/src/native-fusion/session-header.js +101 -2
  53. package/dist/src/native-fusion/state.js +104 -2
  54. package/dist/src/native-fusion/types.js +1 -1
  55. package/dist/src/personal-sync/machine-authority.js +34 -1
  56. package/dist/src/room/activation-audit.js +45 -2
  57. package/dist/src/room/activation-policy.js +30 -1
  58. package/dist/src/room/activation-service.d.ts +1 -0
  59. package/dist/src/room/activation-service.js +408 -8
  60. package/dist/src/room/invite-bootstrap.js +156 -1
  61. package/dist/src/room/managed-context.d.ts +1 -1
  62. package/dist/src/room/managed-context.js +1 -1
  63. package/dist/src/room/managed-prompt-composer.d.ts +1 -1
  64. package/dist/src/room/managed-prompt-composer.js +6 -5
  65. package/dist/src/room/managed-room-binding.js +167 -1
  66. package/dist/src/room/managed-room-ipc.d.ts +2 -2
  67. package/dist/src/room/managed-room-ipc.js +2 -2
  68. package/dist/src/room/mirror-sync-service.js +183 -1
  69. package/dist/src/room/room-action-client.d.ts +11 -0
  70. package/dist/src/room/room-action-client.js +1 -1
  71. package/dist/src/room/room-binding-client.d.ts +1 -0
  72. package/dist/src/room/room-binding-client.js +151 -1
  73. package/dist/src/security/managed-agent-policy.d.ts +3 -2
  74. package/dist/src/security/managed-agent-policy.js +1 -1
  75. package/dist/src/session/archive-zip.js +220 -1
  76. package/dist/src/session/deletion-sync-state.js +136 -2
  77. package/dist/src/session/handlers/agent-capabilities.js +14 -1
  78. package/dist/src/session/handlers/agent-config.js +150 -1
  79. package/dist/src/session/handlers/agents.js +55 -1
  80. package/dist/src/session/handlers/chat.d.ts +1 -1
  81. package/dist/src/session/handlers/chat.js +1030 -2
  82. package/dist/src/session/handlers/control.js +61 -1
  83. package/dist/src/session/handlers/fs.js +594 -1
  84. package/dist/src/session/handlers/message-status.js +15 -1
  85. package/dist/src/session/handlers/remote-access.js +43 -1
  86. package/dist/src/session/handlers/room-managed.js +101 -1
  87. package/dist/src/session/handlers/session-refresh.js +35 -1
  88. package/dist/src/session/handlers/title.js +79 -1
  89. package/dist/src/session/handlers/tool-detail.js +149 -1
  90. package/dist/src/session/handlers/voice-window.js +62 -1
  91. package/dist/src/session/history-subscriptions.js +470 -1
  92. package/dist/src/session/manager.js +658 -1
  93. package/dist/src/session/native-cleanup-outbox.js +287 -2
  94. package/dist/src/session/native-session-lifecycle.js +327 -6
  95. package/dist/src/session/queue.d.ts +5 -0
  96. package/dist/src/session/queue.js +8 -8
  97. package/dist/src/upgrade/scheduler.js +126 -1
  98. package/node_modules/@shennian/wire/dist/agent-workspace.js +1 -1
  99. package/package.json +1 -1
@@ -1 +1,115 @@
1
- import{getAgentConfigSummary as m}from"../config-status.js";import{isCacheFresh as h,readCache as a,writeCache as f}from"./cache.js";import{discoverModelsForAgent as A}from"./discovery.js";import{getAgentRuntimeSummary as y}from"../runtime-compatibility.js";import{resolveManagedAgentRuntimeProtection as v}from"../../security/managed-agent-runtime.js";function x(e){const r=new Set,t=[];for(const n of e)!n.id||r.has(n.id)||(r.add(n.id),t.push(n));return t}function g(e){const t=e.type!=="pi"&&e.type!=="openclaw"?v({agentType:e.type,runtimeVersion:e.version,platform:e.runtimePlatform,isWsl:e.runtimeIsWsl,executableFingerprint:e.executableFingerprint,requireExecutableFingerprint:!0}):null;return{supportsManagedAgent:t?.level==="hard",supportsWorker:e.type!=="pi"&&e.type!=="openclaw",supportsManagerMode:e.type==="codex"||e.type==="claude",...t?{managedAgentProtection:t}:{}}}function I(e){const r=a();return e.map(t=>{const n=m(t.type),s=y(t);return{type:t.type,models:r.agents[t.type]?.models??t.models??[],...g(t),...n?{config:n}:{},...s?{runtime:s}:{}}})}async function q(e,r){const t=a();for(const n of e)delete t.agents[n.type];return f(t),C(e,r)}async function C(e,r){const t=a();let n=!1;const s=[];for(const o of e){const i=t.agents[o.type];let c;if(h(i,o.version))c=i.models;else{let p=null;try{p=x(await A(o,r)),c=p.length>0?p:i?.models??o.models??[]}catch{c=i?.models??o.models??[]}if(p&&p.length>0){const d={version:o.version,refreshedAt:Date.now(),models:c};JSON.stringify(i)!==JSON.stringify(d)&&(t.agents[o.type]=d,n=!0)}}const u=m(o.type),l=y(o);s.push({type:o.type,models:c,...g(o),...u?{config:u}:{},...l?{runtime:l}:{}})}return n&&f(t),s}export{I as getCachedAgentInfos,q as refreshAgentInfos,C as resolveAgentInfos};
1
+ // @arch docs/architecture/cli/model-discovery.md
2
+ // @arch docs/features/managed-agent-restricted-runtime.md
3
+ // @test src/__tests__/model-switching.test.ts
4
+ // @test src/__tests__/model-registry-service.test.ts
5
+ import { getAgentConfigSummary } from '../config-status.js';
6
+ import { isCacheFresh, readCache, writeCache } from './cache.js';
7
+ import { discoverModelsForAgent } from './discovery.js';
8
+ import { getAgentRuntimeSummary } from '../runtime-compatibility.js';
9
+ import { resolveManagedAgentRuntimeProtection } from '../../security/managed-agent-runtime.js';
10
+ function uniqueModels(models) {
11
+ const seen = new Set();
12
+ const unique = [];
13
+ for (const model of models) {
14
+ if (!model.id || seen.has(model.id))
15
+ continue;
16
+ seen.add(model.id);
17
+ unique.push(model);
18
+ }
19
+ return unique;
20
+ }
21
+ function runtimeCapabilities(agent) {
22
+ const managedCandidate = agent.type !== 'pi' && agent.type !== 'openclaw';
23
+ const managedAgentProtection = managedCandidate
24
+ ? resolveManagedAgentRuntimeProtection({
25
+ agentType: agent.type,
26
+ runtimeVersion: agent.version,
27
+ platform: agent.runtimePlatform,
28
+ isWsl: agent.runtimeIsWsl,
29
+ executableFingerprint: agent.executableFingerprint,
30
+ requireExecutableFingerprint: true,
31
+ })
32
+ : null;
33
+ // A Room member can be prompted by every other member. "Partial" protection
34
+ // therefore cannot be converted into an acceptable risk acknowledgement: a
35
+ // readable host credential is an account capability, not merely a file leak.
36
+ const supportsManagedAgent = managedAgentProtection?.level === 'hard';
37
+ return {
38
+ supportsManagedAgent,
39
+ supportsWorker: agent.type !== 'pi' && agent.type !== 'openclaw',
40
+ supportsManagerMode: agent.type === 'codex' || agent.type === 'claude',
41
+ ...(managedAgentProtection
42
+ ? {
43
+ managedAgentProtection,
44
+ }
45
+ : {}),
46
+ };
47
+ }
48
+ export function getCachedAgentInfos(detectedAgents) {
49
+ const cache = readCache();
50
+ return detectedAgents.map((agent) => {
51
+ const config = getAgentConfigSummary(agent.type);
52
+ const runtime = getAgentRuntimeSummary(agent);
53
+ return {
54
+ type: agent.type,
55
+ models: cache.agents[agent.type]?.models ?? agent.models ?? [],
56
+ ...runtimeCapabilities(agent),
57
+ ...(config ? { config } : {}),
58
+ ...(runtime ? { runtime } : {}),
59
+ };
60
+ });
61
+ }
62
+ export async function refreshAgentInfos(detectedAgents, context) {
63
+ const cache = readCache();
64
+ for (const agent of detectedAgents) {
65
+ delete cache.agents[agent.type];
66
+ }
67
+ writeCache(cache);
68
+ return resolveAgentInfos(detectedAgents, context);
69
+ }
70
+ export async function resolveAgentInfos(detectedAgents, context) {
71
+ const cache = readCache();
72
+ let changed = false;
73
+ const infos = [];
74
+ for (const agent of detectedAgents) {
75
+ const cached = cache.agents[agent.type];
76
+ let models;
77
+ if (isCacheFresh(cached, agent.version)) {
78
+ models = cached.models;
79
+ }
80
+ else {
81
+ let discovered = null;
82
+ try {
83
+ discovered = uniqueModels(await discoverModelsForAgent(agent, context));
84
+ models = discovered.length > 0 ? discovered : (cached?.models ?? agent.models ?? []);
85
+ }
86
+ catch {
87
+ models = cached?.models ?? agent.models ?? [];
88
+ }
89
+ if (discovered && discovered.length > 0) {
90
+ const nextEntry = {
91
+ version: agent.version,
92
+ refreshedAt: Date.now(),
93
+ models,
94
+ };
95
+ if (JSON.stringify(cached) !== JSON.stringify(nextEntry)) {
96
+ cache.agents[agent.type] = nextEntry;
97
+ changed = true;
98
+ }
99
+ }
100
+ }
101
+ const config = getAgentConfigSummary(agent.type);
102
+ const runtime = getAgentRuntimeSummary(agent);
103
+ infos.push({
104
+ type: agent.type,
105
+ models,
106
+ ...runtimeCapabilities(agent),
107
+ ...(config ? { config } : {}),
108
+ ...(runtime ? { runtime } : {}),
109
+ });
110
+ }
111
+ if (changed) {
112
+ writeCache(cache);
113
+ }
114
+ return infos;
115
+ }
@@ -1 +1,8 @@
1
- import o from"node:os";import{resolveShennianPath as t}from"../../config/index.js";const n=1,p=1440*60*1e3,s=t("model-cache.json"),C=o.tmpdir();export{p as CACHE_TTL_MS,n as CACHE_VERSION,C as DISCOVERY_WORKDIR,s as MODEL_CACHE_PATH};
1
+ // @arch docs/architecture/cli/model-discovery.md
2
+ // @test src/__tests__/model-switching.test.ts
3
+ import os from 'node:os';
4
+ import { resolveShennianPath } from '../../config/index.js';
5
+ export const CACHE_VERSION = 1;
6
+ export const CACHE_TTL_MS = 24 * 60 * 60 * 1000;
7
+ export const MODEL_CACHE_PATH = resolveShennianPath('model-cache.json');
8
+ export const DISCOVERY_WORKDIR = os.tmpdir();
@@ -1 +1,19 @@
1
- import{getCachedAgentInfos as m,refreshAgentInfos as u,resolveAgentInfos as v}from"./model-registry/service.js";import{applyClaudeModelEnvOverrides as e,discoverClaudeAliasModelsFromEnv as s,fallbackCodexModels as o,fallbackClaudeAliasModels as l,parseClaudeBinaryModels as r,parseClaudeModels as d,parseCodexModels as a,parseCodexAppServerModels as p,parseCursorModels as n,parseGeminiModels as i,parseOpenClawModels as t,stripAnsi as C}from"./model-registry/parsers.js";const f={applyClaudeModelEnvOverrides:e,discoverClaudeAliasModelsFromEnv:s,fallbackCodexModels:o,fallbackClaudeAliasModels:l,parseClaudeBinaryModels:r,parseClaudeModels:d,parseCodexModels:a,parseCodexAppServerModels:p,parseCursorModels:n,parseOpenClawModels:t,parseGeminiModels:i,stripAnsi:C};export{m as getCachedAgentInfos,f as modelSwitchingTestExports,u as refreshAgentInfos,v as resolveAgentInfos};
1
+ // @arch docs/architecture/cli/model-discovery.md
2
+ // @test src/__tests__/model-switching.test.ts
3
+ export { getCachedAgentInfos, refreshAgentInfos, resolveAgentInfos, } from './model-registry/service.js';
4
+ import { applyClaudeModelEnvOverrides, discoverClaudeAliasModelsFromEnv, fallbackCodexModels, fallbackClaudeAliasModels, parseClaudeBinaryModels, parseClaudeModels, parseCodexModels, parseCodexAppServerModels, parseCursorModels, parseGeminiModels, parseOpenClawModels, stripAnsi, } from './model-registry/parsers.js';
5
+ /** Used by Vitest / `model-switching-e2e.ts` — not a stable public API. */
6
+ export const modelSwitchingTestExports = {
7
+ applyClaudeModelEnvOverrides,
8
+ discoverClaudeAliasModelsFromEnv,
9
+ fallbackCodexModels,
10
+ fallbackClaudeAliasModels,
11
+ parseClaudeBinaryModels,
12
+ parseClaudeModels,
13
+ parseCodexModels,
14
+ parseCodexAppServerModels,
15
+ parseCursorModels,
16
+ parseOpenClawModels,
17
+ parseGeminiModels,
18
+ stripAnsi,
19
+ };
@@ -1 +1,237 @@
1
- import v from"node:crypto";import{execFileSync as g}from"node:child_process";import u from"node:fs";import x from"node:os";import c from"node:path";const S=new Set(["codex","claude","workbuddy"]);function P(t){return S.has(t)}function L(t){if(!P(t.agentType))return[];const i=c.resolve(t.homeDir??x.homedir()),e=c.resolve(t.workDir||i),r=[];if(t.agentType==="codex"){for(const o of y(e))l(r,c.join(o,".agents","skills"),{provider:"codex",source:"project",priority:0,invocationPrefix:"$",maxDepth:2});l(r,c.join(i,".agents","skills"),{provider:"codex",source:"personal",priority:1,invocationPrefix:"$",maxDepth:2}),l(r,c.join(i,".codex","skills"),{provider:"codex",source:"personal",priority:1,invocationPrefix:"$",maxDepth:3,systemSegment:".system"}),l(r,"/etc/codex/skills",{provider:"codex",source:"system",priority:3,invocationPrefix:"$",maxDepth:2}),t.homeDir||$(r)}else{const o=t.agentType==="claude"?".claude":".codebuddy";for(const s of y(e))l(r,c.join(s,o,"skills"),{provider:t.agentType,source:"project",priority:0,invocationPrefix:"/",maxDepth:3});l(r,c.join(i,o,"skills"),{provider:t.agentType,source:"personal",priority:1,invocationPrefix:"/",maxDepth:3}),D(r,{provider:t.agentType,homeDir:i,configFolder:o})}const n=new Map;for(const o of r){const s=n.get(o.invocation);(!s||o.priority<s.priority)&&n.set(o.invocation,o)}return[...n.values()].sort((o,s)=>o.name.localeCompare(s.name)||o.invocation.localeCompare(s.invocation)).map(({priority:o,...s})=>s)}function $(t){try{const i=g("codex",["plugin","list","--json"],{encoding:"utf8",timeout:5e3,maxBuffer:4194304,stdio:["ignore","pipe","ignore"]}),e=JSON.parse(i);for(const r of e.installed??[]){const n=typeof r.name=="string"?r.name.trim():"",o=typeof r.source?.path=="string"?r.source.path:"";!n||!/^[a-zA-Z0-9._-]+$/.test(n)||!o||r.installed!==!0||r.enabled!==!0||l(t,c.resolve(o),{provider:"codex",source:"plugin",priority:2,invocationPrefix:"$",pluginName:n,maxDepth:4})}}catch{}}function y(t){const i=[];let e=t;for(let r=0;r<64;r+=1){i.push(e);const n=c.dirname(e);if(n===e)break;e=n}return i}function l(t,i,e){N(i)&&k(i,e.maxDepth,r=>{const n=b(r);if(!n)return;const o=c.relative(i,r).split(c.sep),s=e.systemSegment&&o.includes(e.systemSegment)?"system":e.source,a=e.pluginName?`${e.invocationPrefix}${e.pluginName}:${n.name}`:`${e.invocationPrefix}${n.name}`;t.push({id:j(e.provider,a,r),name:n.name,description:n.description,kind:e.pluginName?"plugin_skill":"skill",source:s,invocation:a,priority:e.priority})})}function D(t,i){const e=c.join(i.homeDir,i.configFolder,"plugins"),r=c.join(e,"installed_plugins.json");if(_(r))try{const n=JSON.parse(u.readFileSync(r,"utf8"));for(const[o,s]of Object.entries(n.plugins??{})){const a=o.split("@")[0]?.trim();if(!a||!/^[a-zA-Z0-9._-]+$/.test(a))continue;const p=Array.isArray(s)?s:[s];for(const d of p){const f=d&&typeof d=="object"&&"installPath"in d?String(d.installPath??""):"";if(!f)continue;const m=c.resolve(f);w(m,e)&&l(t,m,{provider:i.provider,source:"plugin",priority:2,invocationPrefix:"/",pluginName:a,maxDepth:4})}}}catch{}}function k(t,i,e){const r=[{directory:t,depth:0}];let n=0;for(;r.length>0&&n<2e3;){const o=r.shift();let s;try{s=u.readdirSync(o.directory,{withFileTypes:!0})}catch{continue}for(const a of s){if(n+=1,n>=2e3)break;const p=c.join(o.directory,a.name);a.isFile()&&!a.isSymbolicLink()&&a.name==="SKILL.md"&&e(p),a.isDirectory()&&!a.isSymbolicLink()&&o.depth<i&&r.push({directory:p,depth:o.depth+1})}}}function b(t){try{const e=u.readFileSync(t,"utf8").slice(0,65536).match(/^---\s*\r?\n([\s\S]*?)\r?\n---(?:\s*\r?\n|$)/)?.[1]??"",r=h(e,"name")||c.basename(c.dirname(t));if(!/^[a-zA-Z0-9._-]+$/.test(r))return null;const n=h(e,"description");return{name:r,...n?{description:n}:{}}}catch{return null}}function h(t,i){const e=t.split(/\r?\n/).find(n=>n.startsWith(`${i}:`));return e?e.slice(i.length+1).trim().replace(/^(?:"([\s\S]*)"|'([\s\S]*)')$/,"$1$2").trim():""}function j(t,i,e){return v.createHash("sha256").update(`${t}\0${i}\0${e}`).digest("hex").slice(0,20)}function w(t,i){const e=c.relative(c.resolve(i),t);return e===""||!e.startsWith(`..${c.sep}`)&&e!==".."&&!c.isAbsolute(e)}function N(t){try{const i=u.lstatSync(t);return i.isDirectory()&&!i.isSymbolicLink()}catch{return!1}}function _(t){try{const i=u.lstatSync(t);return i.isFile()&&!i.isSymbolicLink()}catch{return!1}}export{P as isNativeCapabilityProvider,L as listAgentNativeCapabilities};
1
+ // @arch docs/features/agent-native-capabilities.md
2
+ // @test src/__tests__/native-capabilities.test.ts
3
+ import crypto from 'node:crypto';
4
+ import { execFileSync } from 'node:child_process';
5
+ import fs from 'node:fs';
6
+ import os from 'node:os';
7
+ import path from 'node:path';
8
+ const SUPPORTED_PROVIDERS = new Set(['codex', 'claude', 'workbuddy']);
9
+ export function isNativeCapabilityProvider(value) {
10
+ return SUPPORTED_PROVIDERS.has(value);
11
+ }
12
+ export function listAgentNativeCapabilities(options) {
13
+ if (!isNativeCapabilityProvider(options.agentType))
14
+ return [];
15
+ const homeDir = path.resolve(options.homeDir ?? os.homedir());
16
+ const workDir = path.resolve(options.workDir || homeDir);
17
+ const candidates = [];
18
+ if (options.agentType === 'codex') {
19
+ for (const root of ancestorRoots(workDir)) {
20
+ collectSkillDirectory(candidates, path.join(root, '.agents', 'skills'), {
21
+ provider: 'codex', source: 'project', priority: 0, invocationPrefix: '$', maxDepth: 2,
22
+ });
23
+ }
24
+ collectSkillDirectory(candidates, path.join(homeDir, '.agents', 'skills'), {
25
+ provider: 'codex', source: 'personal', priority: 1, invocationPrefix: '$', maxDepth: 2,
26
+ });
27
+ // Current Codex hosts also expose built-in and locally installed skills here.
28
+ collectSkillDirectory(candidates, path.join(homeDir, '.codex', 'skills'), {
29
+ provider: 'codex', source: 'personal', priority: 1, invocationPrefix: '$', maxDepth: 3,
30
+ systemSegment: '.system',
31
+ });
32
+ collectSkillDirectory(candidates, '/etc/codex/skills', {
33
+ provider: 'codex', source: 'system', priority: 3, invocationPrefix: '$', maxDepth: 2,
34
+ });
35
+ if (!options.homeDir)
36
+ collectCodexPluginSkills(candidates);
37
+ }
38
+ else {
39
+ const configFolder = options.agentType === 'claude' ? '.claude' : '.codebuddy';
40
+ for (const root of ancestorRoots(workDir)) {
41
+ collectSkillDirectory(candidates, path.join(root, configFolder, 'skills'), {
42
+ provider: options.agentType, source: 'project', priority: 0, invocationPrefix: '/', maxDepth: 3,
43
+ });
44
+ }
45
+ collectSkillDirectory(candidates, path.join(homeDir, configFolder, 'skills'), {
46
+ provider: options.agentType, source: 'personal', priority: 1, invocationPrefix: '/', maxDepth: 3,
47
+ });
48
+ collectInstalledPluginSkills(candidates, {
49
+ provider: options.agentType,
50
+ homeDir,
51
+ configFolder,
52
+ });
53
+ }
54
+ const deduped = new Map();
55
+ for (const candidate of candidates) {
56
+ const current = deduped.get(candidate.invocation);
57
+ if (!current || candidate.priority < current.priority)
58
+ deduped.set(candidate.invocation, candidate);
59
+ }
60
+ return [...deduped.values()]
61
+ .sort((a, b) => a.name.localeCompare(b.name) || a.invocation.localeCompare(b.invocation))
62
+ .map(({ priority: _priority, ...capability }) => capability);
63
+ }
64
+ function collectCodexPluginSkills(target) {
65
+ try {
66
+ const output = execFileSync('codex', ['plugin', 'list', '--json'], {
67
+ encoding: 'utf8',
68
+ timeout: 5_000,
69
+ maxBuffer: 4 * 1024 * 1024,
70
+ stdio: ['ignore', 'pipe', 'ignore'],
71
+ });
72
+ const parsed = JSON.parse(output);
73
+ for (const plugin of parsed.installed ?? []) {
74
+ const pluginName = typeof plugin.name === 'string' ? plugin.name.trim() : '';
75
+ const installPath = typeof plugin.source?.path === 'string' ? plugin.source.path : '';
76
+ if (!pluginName || !/^[a-zA-Z0-9._-]+$/.test(pluginName) || !installPath)
77
+ continue;
78
+ if (plugin.installed !== true || plugin.enabled !== true)
79
+ continue;
80
+ collectSkillDirectory(target, path.resolve(installPath), {
81
+ provider: 'codex',
82
+ source: 'plugin',
83
+ priority: 2,
84
+ invocationPrefix: '$',
85
+ pluginName,
86
+ maxDepth: 4,
87
+ });
88
+ }
89
+ }
90
+ catch {
91
+ // Codex versions without the plugin JSON command still expose filesystem skills.
92
+ }
93
+ }
94
+ function ancestorRoots(start) {
95
+ const roots = [];
96
+ let current = start;
97
+ for (let count = 0; count < 64; count += 1) {
98
+ roots.push(current);
99
+ const parent = path.dirname(current);
100
+ if (parent === current)
101
+ break;
102
+ current = parent;
103
+ }
104
+ return roots;
105
+ }
106
+ function collectSkillDirectory(target, root, options) {
107
+ if (!isRealDirectory(root))
108
+ return;
109
+ walkSkillFiles(root, options.maxDepth, (skillFile) => {
110
+ const metadata = readSkillMetadata(skillFile);
111
+ if (!metadata)
112
+ return;
113
+ const relative = path.relative(root, skillFile).split(path.sep);
114
+ const source = options.systemSegment && relative.includes(options.systemSegment)
115
+ ? 'system'
116
+ : options.source;
117
+ const invocation = options.pluginName
118
+ ? `${options.invocationPrefix}${options.pluginName}:${metadata.name}`
119
+ : `${options.invocationPrefix}${metadata.name}`;
120
+ target.push({
121
+ id: stableCapabilityId(options.provider, invocation, skillFile),
122
+ name: metadata.name,
123
+ description: metadata.description,
124
+ kind: options.pluginName ? 'plugin_skill' : 'skill',
125
+ source,
126
+ invocation,
127
+ priority: options.priority,
128
+ });
129
+ });
130
+ }
131
+ function collectInstalledPluginSkills(target, options) {
132
+ const pluginRoot = path.join(options.homeDir, options.configFolder, 'plugins');
133
+ const manifestPath = path.join(pluginRoot, 'installed_plugins.json');
134
+ if (!isRealFile(manifestPath))
135
+ return;
136
+ try {
137
+ const parsed = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
138
+ for (const [qualifiedName, rawInstallations] of Object.entries(parsed.plugins ?? {})) {
139
+ const pluginName = qualifiedName.split('@')[0]?.trim();
140
+ if (!pluginName || !/^[a-zA-Z0-9._-]+$/.test(pluginName))
141
+ continue;
142
+ const installations = Array.isArray(rawInstallations) ? rawInstallations : [rawInstallations];
143
+ for (const raw of installations) {
144
+ const installPath = raw && typeof raw === 'object' && 'installPath' in raw
145
+ ? String(raw.installPath ?? '')
146
+ : '';
147
+ if (!installPath)
148
+ continue;
149
+ const resolved = path.resolve(installPath);
150
+ if (!isWithinRoot(resolved, pluginRoot))
151
+ continue;
152
+ collectSkillDirectory(target, resolved, {
153
+ provider: options.provider,
154
+ source: 'plugin',
155
+ priority: 2,
156
+ invocationPrefix: '/',
157
+ pluginName,
158
+ maxDepth: 4,
159
+ });
160
+ }
161
+ }
162
+ }
163
+ catch {
164
+ // A malformed third-party plugin registry must not break ordinary chat.
165
+ }
166
+ }
167
+ function walkSkillFiles(root, maxDepth, visit) {
168
+ const queue = [{ directory: root, depth: 0 }];
169
+ let visited = 0;
170
+ while (queue.length > 0 && visited < 2_000) {
171
+ const item = queue.shift();
172
+ let entries;
173
+ try {
174
+ entries = fs.readdirSync(item.directory, { withFileTypes: true });
175
+ }
176
+ catch {
177
+ continue;
178
+ }
179
+ for (const entry of entries) {
180
+ visited += 1;
181
+ if (visited >= 2_000)
182
+ break;
183
+ const fullPath = path.join(item.directory, entry.name);
184
+ if (entry.isFile() && !entry.isSymbolicLink() && entry.name === 'SKILL.md')
185
+ visit(fullPath);
186
+ if (entry.isDirectory() && !entry.isSymbolicLink() && item.depth < maxDepth) {
187
+ queue.push({ directory: fullPath, depth: item.depth + 1 });
188
+ }
189
+ }
190
+ }
191
+ }
192
+ function readSkillMetadata(skillFile) {
193
+ try {
194
+ const content = fs.readFileSync(skillFile, 'utf8').slice(0, 64 * 1024);
195
+ const frontmatter = content.match(/^---\s*\r?\n([\s\S]*?)\r?\n---(?:\s*\r?\n|$)/)?.[1] ?? '';
196
+ const name = readFrontmatterScalar(frontmatter, 'name') || path.basename(path.dirname(skillFile));
197
+ if (!/^[a-zA-Z0-9._-]+$/.test(name))
198
+ return null;
199
+ const description = readFrontmatterScalar(frontmatter, 'description');
200
+ return { name, ...(description ? { description } : {}) };
201
+ }
202
+ catch {
203
+ return null;
204
+ }
205
+ }
206
+ function readFrontmatterScalar(frontmatter, key) {
207
+ const line = frontmatter.split(/\r?\n/).find((value) => value.startsWith(`${key}:`));
208
+ if (!line)
209
+ return '';
210
+ const value = line.slice(key.length + 1).trim();
211
+ return value.replace(/^(?:"([\s\S]*)"|'([\s\S]*)')$/, '$1$2').trim();
212
+ }
213
+ function stableCapabilityId(provider, invocation, skillFile) {
214
+ return crypto.createHash('sha256').update(`${provider}\0${invocation}\0${skillFile}`).digest('hex').slice(0, 20);
215
+ }
216
+ function isWithinRoot(candidate, root) {
217
+ const relative = path.relative(path.resolve(root), candidate);
218
+ return relative === '' || (!relative.startsWith(`..${path.sep}`) && relative !== '..' && !path.isAbsolute(relative));
219
+ }
220
+ function isRealDirectory(value) {
221
+ try {
222
+ const stat = fs.lstatSync(value);
223
+ return stat.isDirectory() && !stat.isSymbolicLink();
224
+ }
225
+ catch {
226
+ return false;
227
+ }
228
+ }
229
+ function isRealFile(value) {
230
+ try {
231
+ const stat = fs.lstatSync(value);
232
+ return stat.isFile() && !stat.isSymbolicLink();
233
+ }
234
+ catch {
235
+ return false;
236
+ }
237
+ }