@canonmsg/codex-plugin 0.18.7 → 0.18.9

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,56 @@ 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 trimmed = prompt.trim();
538
+ const match = trimmed.match(/^\$([A-Za-z0-9:_-]+)(?:\s+([\s\S]*))?$/)
539
+ ?? trimmed.match(/^\/skill\s+([A-Za-z0-9:_-]+)(?:\s+([\s\S]*))?$/);
540
+ if (!match?.[1])
541
+ return null;
542
+ return {
543
+ name: match[1],
544
+ prompt: match[2]?.trim() ?? '',
545
+ };
546
+ }
547
+ function parseSkillsListResponse(result) {
548
+ const entries = Array.isArray(result.data) ? result.data : [];
549
+ const skills = [];
550
+ const seen = new Set();
551
+ for (const entry of entries) {
552
+ if (!isRecord(entry))
553
+ continue;
554
+ const cwdSkills = Array.isArray(entry.skills) ? entry.skills : [];
555
+ for (const rawSkill of cwdSkills) {
556
+ if (!isRecord(rawSkill))
557
+ continue;
558
+ if (rawSkill.enabled === false)
559
+ continue;
560
+ const name = readString(rawSkill, 'name');
561
+ const path = readString(rawSkill, 'path');
562
+ if (!name || !path || seen.has(name.toLowerCase()))
563
+ continue;
564
+ seen.add(name.toLowerCase());
565
+ const interfaceMetadata = isRecord(rawSkill.interface) ? rawSkill.interface : undefined;
566
+ const scope = readNullableString(rawSkill, 'scope');
567
+ const description = readNullableString(rawSkill, 'description');
568
+ const shortDescription = readNullableString(rawSkill, 'shortDescription');
569
+ const displayName = readNullableString(interfaceMetadata, 'displayName');
570
+ skills.push({
571
+ name,
572
+ path,
573
+ ...(scope ? { scope } : {}),
574
+ ...(description ? { description } : {}),
575
+ ...(shortDescription ? { shortDescription } : {}),
576
+ ...(displayName ? { displayName } : {}),
577
+ });
578
+ }
579
+ }
580
+ return skills;
581
+ }
490
582
  function stringifyPreview(value) {
491
583
  if (typeof value === 'string')
492
584
  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 buildCodexSkillCommands(skills: ReadonlyArray<CodexSkillMetadata>): CanonRuntimeCommandDescriptor[];
35
38
  export declare function main(): Promise<void>;
36
39
  export {};
package/dist/host.js CHANGED
@@ -85,6 +85,36 @@ 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 buildCodexSkillCommands(skills) {
90
+ return 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
+ id: `codex-skill-${skill.name}`,
99
+ label: skill.displayName ?? skill.name,
100
+ aliases: [skill.name],
101
+ category: 'skill',
102
+ placements: ['composer_slash', 'command_palette'],
103
+ availability: ['always'],
104
+ trailingTextBehavior: 'send_as_prompt',
105
+ args: [
106
+ {
107
+ id: 'prompt',
108
+ label: 'Prompt',
109
+ kind: 'string',
110
+ captureRemaining: true,
111
+ },
112
+ ],
113
+ ...(description ? { description } : {}),
114
+ dispatch: { kind: 'text_passthrough', template: `$${skill.name} {argument}` },
115
+ };
116
+ });
117
+ }
88
118
  function buildCodexRuntimeDescriptor(input) {
89
119
  const commands = [
90
120
  {
@@ -105,6 +135,10 @@ function buildCodexRuntimeDescriptor(input) {
105
135
  RUNTIME_STOP_ACTION,
106
136
  RUNTIME_STOP_AND_DROP_ACTION,
107
137
  ];
138
+ const skillCommands = input.skills?.length ? buildCodexSkillCommands(input.skills) : [];
139
+ if (skillCommands.length) {
140
+ commands.push(...skillCommands);
141
+ }
108
142
  const descriptor = buildFirstPartyCodingRuntimeDescriptor({
109
143
  clientType: 'codex',
110
144
  models: input.models,
@@ -1437,6 +1471,10 @@ export async function main() {
1437
1471
  console.error(`[canon-codex] [${session.conversationId.slice(0, 8)}] Thread ${event.threadId}`);
1438
1472
  return;
1439
1473
  }
1474
+ if (event.type === 'skills.changed') {
1475
+ void refreshCodexSkillInventory(true).then(() => publishRuntimeHeartbeat());
1476
+ return;
1477
+ }
1440
1478
  if (event.type === 'message') {
1441
1479
  session.turnState = 'streaming';
1442
1480
  markTurnProgress(session);
@@ -1674,7 +1712,8 @@ export async function main() {
1674
1712
  show: stringArgs(args['show-runtime-detail']),
1675
1713
  hide: stringArgs(args['hide-runtime-detail']),
1676
1714
  });
1677
- let runtimeDescriptor = {
1715
+ let codexSkills = [];
1716
+ const buildCurrentRuntimeDescriptor = () => ({
1678
1717
  defaultWorkspaceId: workspaceOptions[0]?.id,
1679
1718
  ...(typeof args.model === 'string' ? { defaultModel: args.model } : {}),
1680
1719
  availableWorkspaces: buildPublicWorkspaceOptions(workspaceOptions),
@@ -1693,8 +1732,32 @@ export async function main() {
1693
1732
  presentation: runtimePresentation,
1694
1733
  supportsPlanMode: useAppServer,
1695
1734
  supportsRichCards: useAppServer,
1735
+ skills: codexSkills,
1696
1736
  }),
1697
- };
1737
+ });
1738
+ let runtimeDescriptor = buildCurrentRuntimeDescriptor();
1739
+ async function refreshCodexSkillInventory(forceReload = false) {
1740
+ if (!useAppServer)
1741
+ return;
1742
+ const probe = new CodexAppServerAdapter({
1743
+ cwd: workingDir,
1744
+ codexBin,
1745
+ model: typeof args.model === 'string' ? args.model : null,
1746
+ configOverrides: args.config ?? [],
1747
+ });
1748
+ try {
1749
+ codexSkills = await probe.listSkills({ forceReload });
1750
+ runtimeDescriptor = buildCurrentRuntimeDescriptor();
1751
+ }
1752
+ catch (error) {
1753
+ codexSkills = [];
1754
+ runtimeDescriptor = buildCurrentRuntimeDescriptor();
1755
+ console.error('[canon-codex] Failed to load Codex skills:', error instanceof Error ? error.message : error);
1756
+ }
1757
+ finally {
1758
+ probe.close();
1759
+ }
1760
+ }
1698
1761
  function applySessionControl(conversationId, control) {
1699
1762
  const session = sessions.get(conversationId);
1700
1763
  if (!session || session.closed)
@@ -1950,51 +2013,7 @@ export async function main() {
1950
2013
  onError: (error) => console.error(`[canon-codex] SSE error: ${error.message}`),
1951
2014
  },
1952
2015
  });
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
- }
2016
+ await refreshCodexSkillInventory();
1998
2017
  try {
1999
2018
  const conversations = await client.getConversations();
2000
2019
  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.9",
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"