@canonmsg/codex-plugin 0.18.7 → 0.18.8

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/adapter.d.ts CHANGED
@@ -32,6 +32,8 @@ export type CodexEvent = {
32
32
  cached_input_tokens?: number;
33
33
  output_tokens?: number;
34
34
  };
35
+ } | {
36
+ type: 'skills.changed';
35
37
  };
36
38
  export interface CodexServerRequest {
37
39
  id: string | number;
@@ -1,4 +1,12 @@
1
1
  import type { CodexApprovalPolicy, CodexEvent, CodexRunTurnOptions, CodexSandboxMode, CodexTurnResult } from './adapter.js';
2
+ export interface CodexSkillMetadata {
3
+ name: string;
4
+ path: string;
5
+ scope?: string;
6
+ description?: string;
7
+ shortDescription?: string;
8
+ displayName?: string;
9
+ }
2
10
  export declare class CodexAppServerAdapter {
3
11
  private readonly cwd;
4
12
  private readonly codexBin;
@@ -26,6 +34,7 @@ export declare class CodexAppServerAdapter {
26
34
  private currentErrorText;
27
35
  private interrupted;
28
36
  private initialized;
37
+ private skillsCache;
29
38
  private messageTextByItem;
30
39
  private planText;
31
40
  constructor(opts: {
@@ -62,6 +71,10 @@ export declare class CodexAppServerAdapter {
62
71
  private rememberResolvedModel;
63
72
  private sandboxPolicyPayload;
64
73
  private buildWritableRoots;
74
+ listSkills(options?: {
75
+ forceReload?: boolean;
76
+ }): Promise<CodexSkillMetadata[]>;
77
+ private buildTurnInput;
65
78
  private ensureStarted;
66
79
  private handleLine;
67
80
  private handleServerRequest;
@@ -27,6 +27,7 @@ export class CodexAppServerAdapter {
27
27
  currentErrorText = null;
28
28
  interrupted = false;
29
29
  initialized = false;
30
+ skillsCache = null;
30
31
  messageTextByItem = new Map();
31
32
  planText = '';
32
33
  constructor(opts) {
@@ -142,10 +143,7 @@ export class CodexAppServerAdapter {
142
143
  turnPromise.catch(() => { });
143
144
  const turnStarted = await this.sendRequest('turn/start', {
144
145
  threadId: this.threadId,
145
- input: [
146
- { type: 'text', text: prompt, text_elements: [] },
147
- ...imagePaths.map((path) => ({ type: 'localImage', path })),
148
- ],
146
+ input: await this.buildTurnInput(prompt, imagePaths),
149
147
  ...(this.model ? { model: this.model } : {}),
150
148
  ...this.sandboxPolicyPayload(_extraAddDirs),
151
149
  collaborationMode: {
@@ -226,6 +224,45 @@ export class CodexAppServerAdapter {
226
224
  }
227
225
  return roots;
228
226
  }
227
+ async listSkills(options = {}) {
228
+ await this.ensureStarted();
229
+ if (!options.forceReload && this.skillsCache) {
230
+ return this.skillsCache;
231
+ }
232
+ const result = await this.sendRequest('skills/list', {
233
+ cwds: [this.cwd],
234
+ ...(options.forceReload ? { forceReload: true } : {}),
235
+ });
236
+ const parsed = parseSkillsListResponse(result);
237
+ this.skillsCache = parsed;
238
+ return parsed;
239
+ }
240
+ async buildTurnInput(prompt, imagePaths) {
241
+ const skillPrompt = parseSkillSlashPrompt(prompt);
242
+ const input = [];
243
+ if (skillPrompt) {
244
+ const skills = await this.listSkills().catch(() => []);
245
+ const selected = skills.find((skill) => skill.name.toLowerCase() === skillPrompt.name.toLowerCase());
246
+ if (selected) {
247
+ input.push({ type: 'skill', name: selected.name, path: selected.path });
248
+ if (skillPrompt.prompt.trim()) {
249
+ input.push({ type: 'text', text: skillPrompt.prompt, text_elements: [] });
250
+ }
251
+ }
252
+ else {
253
+ input.push({
254
+ type: 'text',
255
+ text: `$${skillPrompt.name}${skillPrompt.prompt.trim() ? ` ${skillPrompt.prompt.trim()}` : ''}`,
256
+ text_elements: [],
257
+ });
258
+ }
259
+ }
260
+ else {
261
+ input.push({ type: 'text', text: prompt, text_elements: [] });
262
+ }
263
+ input.push(...imagePaths.map((path) => ({ type: 'localImage', path })));
264
+ return input;
265
+ }
229
266
  async ensureStarted() {
230
267
  if (this.child && this.initialized)
231
268
  return;
@@ -317,6 +354,11 @@ export class CodexAppServerAdapter {
317
354
  this.currentOnEvent?.({ type: 'turn.started' });
318
355
  return;
319
356
  }
357
+ if (method === 'skills/changed') {
358
+ this.skillsCache = null;
359
+ this.currentOnEvent?.({ type: 'skills.changed' });
360
+ return;
361
+ }
320
362
  if (method === 'thread/status/changed') {
321
363
  const status = params.status;
322
364
  if (status?.type === 'active' && Array.isArray(status.activeFlags)) {
@@ -487,6 +529,54 @@ function readRawString(record, key) {
487
529
  const value = record?.[key];
488
530
  return typeof value === 'string' ? value : undefined;
489
531
  }
532
+ function readNullableString(record, key) {
533
+ const value = record?.[key];
534
+ return typeof value === 'string' && value.trim() ? value.trim() : undefined;
535
+ }
536
+ function parseSkillSlashPrompt(prompt) {
537
+ const match = prompt.trim().match(/^\/skill\s+([A-Za-z0-9:_-]+)(?:\s+([\s\S]*))?$/);
538
+ if (!match?.[1])
539
+ return null;
540
+ return {
541
+ name: match[1],
542
+ prompt: match[2]?.trim() ?? '',
543
+ };
544
+ }
545
+ function parseSkillsListResponse(result) {
546
+ const entries = Array.isArray(result.data) ? result.data : [];
547
+ const skills = [];
548
+ const seen = new Set();
549
+ for (const entry of entries) {
550
+ if (!isRecord(entry))
551
+ continue;
552
+ const cwdSkills = Array.isArray(entry.skills) ? entry.skills : [];
553
+ for (const rawSkill of cwdSkills) {
554
+ if (!isRecord(rawSkill))
555
+ continue;
556
+ if (rawSkill.enabled === false)
557
+ continue;
558
+ const name = readString(rawSkill, 'name');
559
+ const path = readString(rawSkill, 'path');
560
+ if (!name || !path || seen.has(name.toLowerCase()))
561
+ continue;
562
+ seen.add(name.toLowerCase());
563
+ const interfaceMetadata = isRecord(rawSkill.interface) ? rawSkill.interface : undefined;
564
+ const scope = readNullableString(rawSkill, 'scope');
565
+ const description = readNullableString(rawSkill, 'description');
566
+ const shortDescription = readNullableString(rawSkill, 'shortDescription');
567
+ const displayName = readNullableString(interfaceMetadata, 'displayName');
568
+ skills.push({
569
+ name,
570
+ path,
571
+ ...(scope ? { scope } : {}),
572
+ ...(description ? { description } : {}),
573
+ ...(shortDescription ? { shortDescription } : {}),
574
+ ...(displayName ? { displayName } : {}),
575
+ });
576
+ }
577
+ }
578
+ return skills;
579
+ }
490
580
  function stringifyPreview(value) {
491
581
  if (typeof value === 'string')
492
582
  return value;
package/dist/host.d.ts CHANGED
@@ -1,4 +1,6 @@
1
1
  #!/usr/bin/env node
2
+ import { type CanonRuntimeCommandDescriptor } from '@canonmsg/core';
3
+ import { type CodexSkillMetadata } from './app-server-adapter.js';
2
4
  interface HostSessionState {
3
5
  lastError?: string;
4
6
  model?: string;
@@ -32,5 +34,6 @@ export declare const CODEX_EFFORT_OPTIONS: readonly [{
32
34
  readonly value: "xhigh";
33
35
  readonly label: "Extra high";
34
36
  }];
37
+ export declare function buildCodexSkillCommand(skills: ReadonlyArray<CodexSkillMetadata>): CanonRuntimeCommandDescriptor | null;
35
38
  export declare function main(): Promise<void>;
36
39
  export {};
package/dist/host.js CHANGED
@@ -85,6 +85,50 @@ export const CODEX_EFFORT_OPTIONS = [
85
85
  { value: 'xhigh', label: 'Extra high' },
86
86
  ];
87
87
  const CODEX_EFFORT_VALUES = new Set(CODEX_EFFORT_OPTIONS.map((option) => option.value));
88
+ const MAX_CODEX_SKILL_COMMAND_CHOICES = 50;
89
+ export function buildCodexSkillCommand(skills) {
90
+ const choices = skills
91
+ .filter((skill) => /^[A-Za-z0-9:_-]+$/.test(skill.name))
92
+ .slice(0, MAX_CODEX_SKILL_COMMAND_CHOICES)
93
+ .map((skill) => {
94
+ const description = skill.shortDescription
95
+ ?? skill.description
96
+ ?? (skill.scope ? `${skill.scope} skill` : undefined);
97
+ return {
98
+ value: skill.name,
99
+ label: skill.displayName ?? skill.name,
100
+ ...(description ? { description } : {}),
101
+ };
102
+ });
103
+ if (choices.length === 0)
104
+ return null;
105
+ return {
106
+ id: 'codex-skill',
107
+ label: 'Use skill',
108
+ description: 'Invoke one of the Codex skills available to this runtime.',
109
+ aliases: ['skill'],
110
+ category: 'skill',
111
+ placements: ['composer_slash', 'command_palette'],
112
+ availability: ['always'],
113
+ trailingTextBehavior: 'send_as_prompt',
114
+ args: [
115
+ {
116
+ id: 'skill',
117
+ label: 'Skill',
118
+ kind: 'enum',
119
+ required: true,
120
+ choices,
121
+ },
122
+ {
123
+ id: 'prompt',
124
+ label: 'Prompt',
125
+ kind: 'string',
126
+ captureRemaining: true,
127
+ },
128
+ ],
129
+ dispatch: { kind: 'text_passthrough', template: '/skill {args.skill} {args.prompt}' },
130
+ };
131
+ }
88
132
  function buildCodexRuntimeDescriptor(input) {
89
133
  const commands = [
90
134
  {
@@ -105,6 +149,10 @@ function buildCodexRuntimeDescriptor(input) {
105
149
  RUNTIME_STOP_ACTION,
106
150
  RUNTIME_STOP_AND_DROP_ACTION,
107
151
  ];
152
+ const skillCommand = input.skills?.length ? buildCodexSkillCommand(input.skills) : null;
153
+ if (skillCommand) {
154
+ commands.push(skillCommand);
155
+ }
108
156
  const descriptor = buildFirstPartyCodingRuntimeDescriptor({
109
157
  clientType: 'codex',
110
158
  models: input.models,
@@ -1437,6 +1485,10 @@ export async function main() {
1437
1485
  console.error(`[canon-codex] [${session.conversationId.slice(0, 8)}] Thread ${event.threadId}`);
1438
1486
  return;
1439
1487
  }
1488
+ if (event.type === 'skills.changed') {
1489
+ void refreshCodexSkillInventory(true).then(() => publishRuntimeHeartbeat());
1490
+ return;
1491
+ }
1440
1492
  if (event.type === 'message') {
1441
1493
  session.turnState = 'streaming';
1442
1494
  markTurnProgress(session);
@@ -1674,7 +1726,8 @@ export async function main() {
1674
1726
  show: stringArgs(args['show-runtime-detail']),
1675
1727
  hide: stringArgs(args['hide-runtime-detail']),
1676
1728
  });
1677
- let runtimeDescriptor = {
1729
+ let codexSkills = [];
1730
+ const buildCurrentRuntimeDescriptor = () => ({
1678
1731
  defaultWorkspaceId: workspaceOptions[0]?.id,
1679
1732
  ...(typeof args.model === 'string' ? { defaultModel: args.model } : {}),
1680
1733
  availableWorkspaces: buildPublicWorkspaceOptions(workspaceOptions),
@@ -1693,8 +1746,32 @@ export async function main() {
1693
1746
  presentation: runtimePresentation,
1694
1747
  supportsPlanMode: useAppServer,
1695
1748
  supportsRichCards: useAppServer,
1749
+ skills: codexSkills,
1696
1750
  }),
1697
- };
1751
+ });
1752
+ let runtimeDescriptor = buildCurrentRuntimeDescriptor();
1753
+ async function refreshCodexSkillInventory(forceReload = false) {
1754
+ if (!useAppServer)
1755
+ return;
1756
+ const probe = new CodexAppServerAdapter({
1757
+ cwd: workingDir,
1758
+ codexBin,
1759
+ model: typeof args.model === 'string' ? args.model : null,
1760
+ configOverrides: args.config ?? [],
1761
+ });
1762
+ try {
1763
+ codexSkills = await probe.listSkills({ forceReload });
1764
+ runtimeDescriptor = buildCurrentRuntimeDescriptor();
1765
+ }
1766
+ catch (error) {
1767
+ codexSkills = [];
1768
+ runtimeDescriptor = buildCurrentRuntimeDescriptor();
1769
+ console.error('[canon-codex] Failed to load Codex skills:', error instanceof Error ? error.message : error);
1770
+ }
1771
+ finally {
1772
+ probe.close();
1773
+ }
1774
+ }
1698
1775
  function applySessionControl(conversationId, control) {
1699
1776
  const session = sessions.get(conversationId);
1700
1777
  if (!session || session.closed)
@@ -1950,51 +2027,7 @@ export async function main() {
1950
2027
  onError: (error) => console.error(`[canon-codex] SSE error: ${error.message}`),
1951
2028
  },
1952
2029
  });
1953
- try {
1954
- runtimeDescriptor = {
1955
- defaultWorkspaceId: workspaceOptions[0]?.id,
1956
- ...(typeof args.model === 'string' ? { defaultModel: args.model } : {}),
1957
- availableWorkspaces: buildPublicWorkspaceOptions(workspaceOptions),
1958
- availableExecutionModes: hostAvailableExecutionModes,
1959
- availablePermissionModes: [...codexPermissionEnvelope.availablePermissionModes],
1960
- ...(codexPermissionEnvelope.defaultPermissionMode
1961
- ? { defaultPermissionMode: codexPermissionEnvelope.defaultPermissionMode }
1962
- : {}),
1963
- runtimeDescriptor: buildCodexRuntimeDescriptor({
1964
- models: codexModelOptions,
1965
- workspaces: buildPublicWorkspaceOptions(workspaceOptions),
1966
- workspaceRoots: workspaceRootMetadata,
1967
- executionModes: hostAvailableExecutionModes,
1968
- permissionModes: [...codexPermissionEnvelope.availablePermissionModes],
1969
- defaultPermissionMode: codexPermissionEnvelope.defaultPermissionMode,
1970
- presentation: runtimePresentation,
1971
- supportsPlanMode: useAppServer,
1972
- supportsRichCards: useAppServer,
1973
- }),
1974
- };
1975
- }
1976
- catch {
1977
- runtimeDescriptor = {
1978
- defaultWorkspaceId: workspaceOptions[0]?.id,
1979
- availableWorkspaces: buildPublicWorkspaceOptions(workspaceOptions),
1980
- availableExecutionModes: hostAvailableExecutionModes,
1981
- availablePermissionModes: [...codexPermissionEnvelope.availablePermissionModes],
1982
- ...(codexPermissionEnvelope.defaultPermissionMode
1983
- ? { defaultPermissionMode: codexPermissionEnvelope.defaultPermissionMode }
1984
- : {}),
1985
- runtimeDescriptor: buildCodexRuntimeDescriptor({
1986
- models: codexModelOptions,
1987
- workspaces: buildPublicWorkspaceOptions(workspaceOptions),
1988
- workspaceRoots: workspaceRootMetadata,
1989
- executionModes: hostAvailableExecutionModes,
1990
- permissionModes: [...codexPermissionEnvelope.availablePermissionModes],
1991
- defaultPermissionMode: codexPermissionEnvelope.defaultPermissionMode,
1992
- presentation: runtimePresentation,
1993
- supportsPlanMode: useAppServer,
1994
- supportsRichCards: useAppServer,
1995
- }),
1996
- };
1997
- }
2030
+ await refreshCodexSkillInventory();
1998
2031
  try {
1999
2032
  const conversations = await client.getConversations();
2000
2033
  lastKnownConversationRefreshAt = Date.now();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@canonmsg/codex-plugin",
3
- "version": "0.18.7",
3
+ "version": "0.18.8",
4
4
  "description": "Canon host integration for Codex CLI",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -29,9 +29,9 @@
29
29
  "prepack": "npm run build"
30
30
  },
31
31
  "dependencies": {
32
- "@canonmsg/agent-sdk": "^3.3.1",
32
+ "@canonmsg/agent-sdk": "^3.4.1",
33
33
  "@canonmsg/coding-agent-host": "^0.2.2",
34
- "@canonmsg/core": "^2.8.1"
34
+ "@canonmsg/core": "^2.9.2"
35
35
  },
36
36
  "engines": {
37
37
  "node": ">=18.0.0"