pattyeng 1.0.4 → 1.0.7

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/README.md +103 -43
  2. package/bin/pattyeng.js +25 -14
  3. package/lib/skills.js +198 -0
  4. package/lib/upgrade.js +46 -0
  5. package/package.json +5 -3
  6. package/skills/.system/.codex-system-skills.marker +1 -0
  7. package/skills/.system/imagegen/LICENSE.txt +201 -0
  8. package/skills/.system/imagegen/SKILL.md +356 -0
  9. package/skills/.system/imagegen/agents/openai.yaml +6 -0
  10. package/skills/.system/imagegen/assets/imagegen-small.svg +5 -0
  11. package/skills/.system/imagegen/assets/imagegen.png +0 -0
  12. package/skills/.system/imagegen/references/cli.md +242 -0
  13. package/skills/.system/imagegen/references/codex-network.md +33 -0
  14. package/skills/.system/imagegen/references/image-api.md +90 -0
  15. package/skills/.system/imagegen/references/prompting.md +118 -0
  16. package/skills/.system/imagegen/references/sample-prompts.md +433 -0
  17. package/skills/.system/imagegen/scripts/image_gen.py +995 -0
  18. package/skills/.system/imagegen/scripts/remove_chroma_key.py +440 -0
  19. package/skills/.system/openai-docs/LICENSE.txt +201 -0
  20. package/skills/.system/openai-docs/SKILL.md +167 -0
  21. package/skills/.system/openai-docs/agents/openai.yaml +14 -0
  22. package/skills/.system/openai-docs/assets/openai-small.svg +3 -0
  23. package/skills/.system/openai-docs/assets/openai.png +0 -0
  24. package/skills/.system/openai-docs/references/latest-model.md +37 -0
  25. package/skills/.system/openai-docs/references/prompting-guide.md +244 -0
  26. package/skills/.system/openai-docs/references/upgrade-guide.md +181 -0
  27. package/skills/.system/openai-docs/scripts/fetch-codex-manual.mjs +598 -0
  28. package/skills/.system/openai-docs/scripts/resolve-latest-model-info.js +147 -0
  29. package/skills/.system/plugin-creator/SKILL.md +243 -0
  30. package/skills/.system/plugin-creator/agents/openai.yaml +6 -0
  31. package/skills/.system/plugin-creator/assets/plugin-creator-small.svg +3 -0
  32. package/skills/.system/plugin-creator/assets/plugin-creator.png +0 -0
  33. package/skills/.system/plugin-creator/references/installing-and-updating.md +143 -0
  34. package/skills/.system/plugin-creator/references/plugin-json-spec.md +194 -0
  35. package/skills/.system/plugin-creator/scripts/create_basic_plugin.py +324 -0
  36. package/skills/.system/plugin-creator/scripts/read_marketplace_name.py +48 -0
  37. package/skills/.system/plugin-creator/scripts/update_plugin_cachebuster.py +78 -0
  38. package/skills/.system/plugin-creator/scripts/validate_plugin.py +586 -0
  39. package/skills/.system/skill-creator/SKILL.md +416 -0
  40. package/skills/.system/skill-creator/agents/openai.yaml +5 -0
  41. package/skills/.system/skill-creator/assets/skill-creator-small.svg +3 -0
  42. package/skills/.system/skill-creator/assets/skill-creator.png +0 -0
  43. package/skills/.system/skill-creator/license.txt +202 -0
  44. package/skills/.system/skill-creator/references/openai_yaml.md +49 -0
  45. package/skills/.system/skill-creator/scripts/generate_openai_yaml.py +226 -0
  46. package/skills/.system/skill-creator/scripts/init_skill.py +400 -0
  47. package/skills/.system/skill-creator/scripts/quick_validate.py +101 -0
  48. package/skills/.system/skill-installer/LICENSE.txt +202 -0
  49. package/skills/.system/skill-installer/SKILL.md +58 -0
  50. package/skills/.system/skill-installer/agents/openai.yaml +5 -0
  51. package/skills/.system/skill-installer/assets/skill-installer-small.svg +3 -0
  52. package/skills/.system/skill-installer/assets/skill-installer.png +0 -0
  53. package/skills/.system/skill-installer/scripts/github_utils.py +21 -0
  54. package/skills/.system/skill-installer/scripts/install-skill-from-github.py +308 -0
  55. package/skills/.system/skill-installer/scripts/list-skills.py +107 -0
  56. package/skills/cap/SKILL.md +77 -0
  57. package/skills/caveman/SKILL.md +49 -0
  58. package/skills/code-review/SKILL.md +46 -0
  59. package/skills/codebook/SKILL.md +184 -0
  60. package/skills/doc-review/SKILL.md +147 -0
  61. package/skills/feature-research/SKILL.md +34 -0
  62. package/skills/file-op/SKILL.md +116 -0
  63. package/skills/grill-me/SKILL.md +10 -0
  64. package/skills/grill-with-docs/ADR-FORMAT.md +47 -0
  65. package/skills/grill-with-docs/CONTEXT-FORMAT.md +60 -0
  66. package/skills/grill-with-docs/SKILL.md +88 -0
  67. package/skills/handoff/SKILL.md +17 -0
  68. package/skills/improve-codebase/SKILL.md +79 -0
  69. package/skills/linear/SKILL.md +107 -0
  70. package/skills/next-step/SKILL.md +61 -0
  71. package/skills/no-reinvent/SKILL.md +131 -0
  72. package/skills/no-wall/SKILL.md +146 -0
  73. package/skills/online/SKILL.md +54 -0
  74. package/skills/plan-review/SKILL.md +55 -0
  75. package/skills/professional/SKILL.md +111 -0
  76. package/skills/promise-tracking/SKILL.md +131 -0
  77. package/skills/prove/SKILL.md +135 -0
  78. package/skills/read-before-write/SKILL.md +123 -0
  79. package/skills/review/SKILL.md +78 -0
  80. package/skills/review-mp/SKILL.md +78 -0
  81. package/skills/scope-boundary/SKILL.md +121 -0
  82. package/skills/semble-search/SKILL.md +40 -0
  83. package/skills/sitrep/SKILL.md +126 -0
  84. package/skills/structure-code/SKILL.md +116 -0
  85. package/skills/surface-assumptions/SKILL.md +139 -0
  86. package/skills/teach/GLOSSARY-FORMAT.md +35 -0
  87. package/skills/teach/LEARNING-RECORD-FORMAT.md +46 -0
  88. package/skills/teach/MISSION-FORMAT.md +31 -0
  89. package/skills/teach/RESOURCES-FORMAT.md +32 -0
  90. package/skills/teach/SKILL.md +131 -0
  91. package/skills/team-protocol/SKILL.md +172 -0
  92. package/skills/tech-lead/SKILL.md +332 -0
  93. package/skills/to-issues/SKILL.md +83 -0
  94. package/skills/to-prd/SKILL.md +74 -0
  95. package/skills/vectorbt-expert/SKILL.md +253 -0
  96. package/skills/verify-done/SKILL.md +128 -0
  97. package/skills/work-log/SKILL.md +141 -0
  98. package/skills/work-log/template.html +496 -0
  99. package/skills/zoom-out/SKILL.md +7 -0
@@ -0,0 +1,194 @@
1
+ # Plugin JSON sample spec
2
+
3
+ ```json
4
+ {
5
+ "name": "plugin-name",
6
+ "version": "1.2.0",
7
+ "description": "Brief plugin description",
8
+ "author": {
9
+ "name": "Author Name",
10
+ "email": "author@example.com",
11
+ "url": "https://github.com/author"
12
+ },
13
+ "homepage": "https://docs.example.com/plugin",
14
+ "repository": "https://github.com/author/plugin",
15
+ "license": "MIT",
16
+ "keywords": ["keyword1", "keyword2"],
17
+ "skills": "./skills/",
18
+ "hooks": "./hooks.json",
19
+ "mcpServers": "./.mcp.json",
20
+ "apps": "./.app.json",
21
+ "interface": {
22
+ "displayName": "Plugin Display Name",
23
+ "shortDescription": "Short description for subtitle",
24
+ "longDescription": "Long description for details page",
25
+ "developerName": "OpenAI",
26
+ "category": "Productivity",
27
+ "capabilities": ["Interactive", "Write"],
28
+ "websiteURL": "https://openai.com/",
29
+ "privacyPolicyURL": "https://openai.com/policies/row-privacy-policy/",
30
+ "termsOfServiceURL": "https://openai.com/policies/row-terms-of-use/",
31
+ "defaultPrompt": [
32
+ "Summarize my inbox and draft replies for me.",
33
+ "Find open bugs and turn them into Linear tickets.",
34
+ "Review today's meetings and flag scheduling gaps."
35
+ ],
36
+ "brandColor": "#3B82F6",
37
+ "composerIcon": "./assets/icon.png",
38
+ "logo": "./assets/logo.png",
39
+ "screenshots": [
40
+ "./assets/screenshot1.png",
41
+ "./assets/screenshot2.png",
42
+ "./assets/screenshot3.png"
43
+ ]
44
+ }
45
+ }
46
+ ```
47
+
48
+ ## Field guide
49
+
50
+ ### Top-level fields
51
+
52
+ - `name` (`string`): Plugin identifier (kebab-case, no spaces). Required if `plugin.json` is provided and used as manifest name and component namespace.
53
+ - `version` (`string`): Plugin semantic version.
54
+ - `description` (`string`): Short purpose summary.
55
+ - `author` (`object`): Publisher identity.
56
+ - `name` (`string`): Author or team name.
57
+ - `email` (`string`): Contact email.
58
+ - `url` (`string`): Author/team homepage or profile URL.
59
+ - `homepage` (`string`): Documentation URL for plugin usage.
60
+ - `repository` (`string`): Source code URL.
61
+ - `license` (`string`): License identifier (for example `MIT`, `Apache-2.0`).
62
+ - `keywords` (`array` of `string`): Search/discovery tags.
63
+ - `skills` (`string`): Relative path to skill directories/files.
64
+ - `hooks` (`string`): Hook config path.
65
+ - `mcpServers` (`string`): MCP config path.
66
+ - `apps` (`string`): App manifest path for plugin integrations.
67
+ - `interface` (`object`): Interface/UX metadata block for plugin presentation.
68
+
69
+ ### `interface` fields
70
+
71
+ - `displayName` (`string`): User-facing title shown for the plugin.
72
+ - `shortDescription` (`string`): Brief subtitle used in compact views.
73
+ - `longDescription` (`string`): Longer description used on details screens.
74
+ - `developerName` (`string`): Human-readable publisher name.
75
+ - `category` (`string`): Plugin category bucket.
76
+ - `capabilities` (`array` of `string`): Capability list from implementation.
77
+ - `websiteURL` (`string`): Public website for the plugin.
78
+ - `privacyPolicyURL` (`string`): Privacy policy URL.
79
+ - `termsOfServiceURL` (`string`): Terms of service URL.
80
+ - `defaultPrompt` (`array` of `string`): Starter prompts shown in composer/UX context.
81
+ - Include at most 3 strings. Entries after the first 3 are ignored and will not be included.
82
+ - Each string is capped at 128 characters. Longer entries are truncated.
83
+ - Prefer short starter prompts around 50 characters so they scan well in the UI.
84
+ - `brandColor` (`string`): Theme color for the plugin card.
85
+ - `composerIcon` (`string`): Path to icon asset.
86
+ - `logo` (`string`): Path to logo asset.
87
+ - `screenshots` (`array` of `string`): List of screenshot asset paths.
88
+ - Screenshot entries must be PNG filenames and stored under `./assets/`.
89
+ - Keep file paths relative to plugin root.
90
+
91
+ ### Path conventions and defaults
92
+
93
+ - Path values should be relative and begin with `./`.
94
+ - `skills`, `hooks`, and `mcpServers` are supplemented on top of default component discovery; they do not replace defaults.
95
+ - Custom path values must follow the plugin root convention and naming/namespacing rules.
96
+ - This repo’s scaffold writes `.codex-plugin/plugin.json`; treat that as the manifest location this skill generates.
97
+
98
+ # Marketplace JSON sample spec
99
+
100
+ `marketplace.json` depends on where the plugin should live. New plugin creation defaults to the
101
+ personal marketplace unless the caller explicitly requests a repo-local destination:
102
+
103
+ - Personal plugin: `~/.agents/plugins/marketplace.json`
104
+ - Repo/team plugin: `<repo-root>/.agents/plugins/marketplace.json`
105
+
106
+ ```json
107
+ {
108
+ "name": "openai-curated",
109
+ "interface": {
110
+ "displayName": "ChatGPT Official"
111
+ },
112
+ "plugins": [
113
+ {
114
+ "name": "linear",
115
+ "source": {
116
+ "source": "local",
117
+ "path": "./plugins/linear"
118
+ },
119
+ "policy": {
120
+ "installation": "AVAILABLE",
121
+ "authentication": "ON_INSTALL"
122
+ },
123
+ "category": "Productivity"
124
+ }
125
+ ]
126
+ }
127
+ ```
128
+
129
+ ## Marketplace field guide
130
+
131
+ ### Top-level fields
132
+
133
+ - `name` (`string`): Marketplace identifier or catalog name.
134
+ - `interface` (`object`, optional): Marketplace presentation metadata.
135
+ - `plugins` (`array`): Ordered plugin entries. This order determines how Codex renders plugins.
136
+
137
+ ### `interface` fields
138
+
139
+ - `displayName` (`string`, optional): User-facing marketplace title.
140
+
141
+ ### Plugin entry fields
142
+
143
+ - `name` (`string`): Plugin identifier. Match the plugin folder name and `plugin.json` `name`.
144
+ - `source` (`object`): Plugin source descriptor.
145
+ - `source` (`string`): Use `local` for this repo workflow.
146
+ - `path` (`string`): Relative plugin path based on the marketplace root.
147
+ - Personal plugin in `~/.agents/plugins/marketplace.json`: `./plugins/<plugin-name>`
148
+ - Repo/team plugin: `./plugins/<plugin-name>`
149
+ - The same relative path convention is used for both personal and repo/team marketplaces.
150
+ - Example: with `~/.agents/plugins/marketplace.json`, `./plugins/<plugin-name>` resolves to
151
+ `~/plugins/<plugin-name>`.
152
+ - `policy` (`object`): Marketplace policy block. Always include it.
153
+ - `installation` (`string`): Availability policy.
154
+ - Allowed values: `NOT_AVAILABLE`, `AVAILABLE`, `INSTALLED_BY_DEFAULT`
155
+ - Default for new entries: `AVAILABLE`
156
+ - `authentication` (`string`): Authentication timing policy.
157
+ - Allowed values: `ON_INSTALL`, `ON_USE`
158
+ - Default for new entries: `ON_INSTALL`
159
+ - `products` (`array` of `string`, optional): Product override for this plugin entry. Omit it unless product gating is explicitly requested.
160
+ - `category` (`string`): Display category bucket. Always include it.
161
+
162
+ ### Marketplace generation rules
163
+
164
+ - `displayName` belongs under the top-level `interface` object, not individual plugin entries.
165
+ - When creating a new marketplace file from scratch, seed `interface.displayName` alongside top-level `name`.
166
+ - Always include `policy.installation`, `policy.authentication`, and `category` on every generated or updated plugin entry.
167
+ - Treat `policy.products` as an override and omit it unless explicitly requested.
168
+ - Append new entries unless the user explicitly requests reordering.
169
+ - Replace an existing entry for the same plugin only when overwrite is intentional.
170
+ - Default new plugin creation to the personal marketplace.
171
+ - Use a repo/team marketplace only when the user specifically requests that destination.
172
+ - Only override the marketplace `name` when the default `personal` name is already taken or
173
+ installed and you need to seed a different new marketplace file.
174
+ - Choose marketplace location to match the selected destination:
175
+ - Personal plugin: `~/.agents/plugins/marketplace.json`
176
+ - Repo/team plugin: `<repo-root>/.agents/plugins/marketplace.json`
177
+
178
+ ### Plugin validation notes
179
+
180
+ - The validator mirrors the workspace plugin ingestion schema so generated plugins follow the same
181
+ manifest contract from the start.
182
+ - Plugin manifests must include real values for `name`, `version`, `description`,
183
+ `author.name`, and the required `interface` fields.
184
+ - `version` must use strict semver.
185
+ - `websiteURL`, `privacyPolicyURL`, and `termsOfServiceURL` must be absolute `https://` URLs when
186
+ present.
187
+ - `composerIcon`, `logo`, and `screenshots` must point to real files inside the plugin archive when
188
+ present.
189
+ - `apps` and `mcpServers` should appear in `plugin.json` only when `.app.json` and `.mcp.json`
190
+ actually exist.
191
+ - Validation rejects unsupported manifest fields such as `hooks`, so the scaffold keeps them out of
192
+ generated manifests.
193
+ - Run `scripts/validate_plugin.py <plugin-path>` before handing back a generated plugin. It adds one
194
+ intentional preflight check that rejects leftover `[TODO: ...]` placeholders.
@@ -0,0 +1,324 @@
1
+ #!/usr/bin/env python3
2
+ """Scaffold a plugin directory and optionally update marketplace.json."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import argparse
7
+ import json
8
+ import re
9
+ from pathlib import Path
10
+ from typing import Any
11
+
12
+
13
+ MAX_PLUGIN_NAME_LENGTH = 64
14
+ DEFAULT_INSTALL_POLICY = "AVAILABLE"
15
+ DEFAULT_AUTH_POLICY = "ON_INSTALL"
16
+ DEFAULT_CATEGORY = "Productivity"
17
+ DEFAULT_MARKETPLACE_NAME = "personal"
18
+ VALID_INSTALL_POLICIES = {"NOT_AVAILABLE", "AVAILABLE", "INSTALLED_BY_DEFAULT"}
19
+ VALID_AUTH_POLICIES = {"ON_INSTALL", "ON_USE"}
20
+ DEFAULT_PLUGIN_PARENT = Path.home() / "plugins"
21
+ DEFAULT_MARKETPLACE_PATH = Path.home() / ".agents" / "plugins" / "marketplace.json"
22
+
23
+
24
+ def normalize_plugin_name(plugin_name: str) -> str:
25
+ """Normalize a plugin name to lowercase hyphen-case."""
26
+ normalized = plugin_name.strip().lower()
27
+ normalized = re.sub(r"[^a-z0-9]+", "-", normalized)
28
+ normalized = normalized.strip("-")
29
+ normalized = re.sub(r"-{2,}", "-", normalized)
30
+ return normalized
31
+
32
+
33
+ def validate_plugin_name(plugin_name: str) -> None:
34
+ if not plugin_name:
35
+ raise ValueError("Plugin name must include at least one letter or digit.")
36
+ if len(plugin_name) > MAX_PLUGIN_NAME_LENGTH:
37
+ raise ValueError(
38
+ f"Plugin name '{plugin_name}' is too long ({len(plugin_name)} characters). "
39
+ f"Maximum is {MAX_PLUGIN_NAME_LENGTH} characters."
40
+ )
41
+
42
+
43
+ def validate_marketplace_name(marketplace_name: str) -> None:
44
+ if not marketplace_name:
45
+ raise ValueError("Marketplace name must include at least one letter or digit.")
46
+ if re.fullmatch(r"[A-Za-z0-9_-]+", marketplace_name) is None:
47
+ raise ValueError(
48
+ "Marketplace name may only contain ASCII letters, digits, `_`, and `-`."
49
+ )
50
+
51
+
52
+ def display_name_from_plugin_name(plugin_name: str) -> str:
53
+ return " ".join(part.capitalize() for part in re.split(r"[-_]+", plugin_name))
54
+
55
+
56
+ def build_plugin_json(plugin_name: str, *, with_mcp: bool, with_apps: bool) -> dict[str, Any]:
57
+ display_name = display_name_from_plugin_name(plugin_name)
58
+ payload: dict[str, Any] = {
59
+ "name": plugin_name,
60
+ "version": "0.1.0",
61
+ "description": f"{display_name} plugin",
62
+ "author": {
63
+ "name": "Local developer",
64
+ },
65
+ "skills": "./skills/",
66
+ "interface": {
67
+ "displayName": display_name,
68
+ "shortDescription": f"Use {display_name} in Codex.",
69
+ "longDescription": f"{display_name} adds a local Codex plugin scaffold.",
70
+ "developerName": "Local developer",
71
+ "category": DEFAULT_CATEGORY,
72
+ "capabilities": [],
73
+ "defaultPrompt": f"Help me use {display_name}.",
74
+ },
75
+ }
76
+ if with_mcp:
77
+ payload["mcpServers"] = "./.mcp.json"
78
+ if with_apps:
79
+ payload["apps"] = "./.app.json"
80
+ return payload
81
+
82
+
83
+ def build_marketplace_entry(
84
+ plugin_name: str,
85
+ install_policy: str,
86
+ auth_policy: str,
87
+ category: str,
88
+ ) -> dict[str, Any]:
89
+ return {
90
+ "name": plugin_name,
91
+ "source": {
92
+ "source": "local",
93
+ "path": f"./plugins/{plugin_name}",
94
+ },
95
+ "policy": {
96
+ "installation": install_policy,
97
+ "authentication": auth_policy,
98
+ },
99
+ "category": category,
100
+ }
101
+
102
+
103
+ def load_json(path: Path) -> dict[str, Any]:
104
+ with path.open() as handle:
105
+ return json.load(handle)
106
+
107
+
108
+ def build_default_marketplace(marketplace_name: str) -> dict[str, Any]:
109
+ return {
110
+ "name": marketplace_name,
111
+ "interface": {
112
+ "displayName": display_name_from_plugin_name(marketplace_name),
113
+ },
114
+ "plugins": [],
115
+ }
116
+
117
+
118
+ def validate_marketplace_interface(payload: dict[str, Any]) -> None:
119
+ interface = payload.get("interface")
120
+ if interface is not None and not isinstance(interface, dict):
121
+ raise ValueError("marketplace.json field 'interface' must be an object.")
122
+
123
+
124
+ def update_marketplace_json(
125
+ marketplace_path: Path,
126
+ marketplace_name: str | None,
127
+ plugin_name: str,
128
+ install_policy: str,
129
+ auth_policy: str,
130
+ category: str,
131
+ force: bool,
132
+ ) -> None:
133
+ if marketplace_path.exists():
134
+ payload = load_json(marketplace_path)
135
+ else:
136
+ payload = build_default_marketplace(marketplace_name or DEFAULT_MARKETPLACE_NAME)
137
+
138
+ if not isinstance(payload, dict):
139
+ raise ValueError(f"{marketplace_path} must contain a JSON object.")
140
+
141
+ validate_marketplace_interface(payload)
142
+
143
+ existing_marketplace_name = payload.get("name")
144
+ if marketplace_name is not None:
145
+ if not isinstance(existing_marketplace_name, str) or not existing_marketplace_name.strip():
146
+ raise ValueError(f"{marketplace_path} must contain a non-empty string 'name'.")
147
+ if existing_marketplace_name != marketplace_name:
148
+ raise ValueError(
149
+ f"{marketplace_path} already uses marketplace name "
150
+ f"'{existing_marketplace_name}'. Create a new marketplace file to use "
151
+ f"'{marketplace_name}' instead."
152
+ )
153
+
154
+ plugins = payload.setdefault("plugins", [])
155
+ if not isinstance(plugins, list):
156
+ raise ValueError(f"{marketplace_path} field 'plugins' must be an array.")
157
+
158
+ new_entry = build_marketplace_entry(plugin_name, install_policy, auth_policy, category)
159
+
160
+ for index, entry in enumerate(plugins):
161
+ if isinstance(entry, dict) and entry.get("name") == plugin_name:
162
+ if not force:
163
+ raise FileExistsError(
164
+ f"Marketplace entry '{plugin_name}' already exists in {marketplace_path}. "
165
+ "Use --force to overwrite that entry."
166
+ )
167
+ plugins[index] = new_entry
168
+ break
169
+ else:
170
+ plugins.append(new_entry)
171
+
172
+ write_json(marketplace_path, payload, force=True)
173
+
174
+
175
+ def write_json(path: Path, data: dict, force: bool) -> None:
176
+ if path.exists() and not force:
177
+ raise FileExistsError(f"{path} already exists. Use --force to overwrite.")
178
+ path.parent.mkdir(parents=True, exist_ok=True)
179
+ with path.open("w") as handle:
180
+ json.dump(data, handle, indent=2)
181
+ handle.write("\n")
182
+
183
+
184
+ def create_stub_file(path: Path, payload: dict, force: bool) -> None:
185
+ if path.exists() and not force:
186
+ return
187
+ path.parent.mkdir(parents=True, exist_ok=True)
188
+ with path.open("w") as handle:
189
+ json.dump(payload, handle, indent=2)
190
+ handle.write("\n")
191
+
192
+
193
+ def parse_args() -> argparse.Namespace:
194
+ parser = argparse.ArgumentParser(
195
+ description="Create a plugin skeleton with a validation-ready plugin.json."
196
+ )
197
+ parser.add_argument("plugin_name")
198
+ parser.add_argument(
199
+ "--path",
200
+ default=str(DEFAULT_PLUGIN_PARENT),
201
+ help=(
202
+ "Parent directory for plugin creation (defaults to <home>/plugins). "
203
+ "Pass an explicit repo path only when a repo/team plugin is intended."
204
+ ),
205
+ )
206
+ parser.add_argument("--with-skills", action="store_true", help="Create skills/ directory")
207
+ parser.add_argument("--with-hooks", action="store_true", help="Create hooks/ directory")
208
+ parser.add_argument("--with-scripts", action="store_true", help="Create scripts/ directory")
209
+ parser.add_argument("--with-assets", action="store_true", help="Create assets/ directory")
210
+ parser.add_argument("--with-mcp", action="store_true", help="Create .mcp.json placeholder")
211
+ parser.add_argument("--with-apps", action="store_true", help="Create .app.json placeholder")
212
+ parser.add_argument(
213
+ "--with-marketplace",
214
+ action="store_true",
215
+ help=(
216
+ "Create or update <home>/.agents/plugins/marketplace.json by default. "
217
+ "Marketplace entries always point to ./plugins/<plugin-name> relative to the "
218
+ "marketplace root."
219
+ ),
220
+ )
221
+ parser.add_argument(
222
+ "--marketplace-path",
223
+ default=str(DEFAULT_MARKETPLACE_PATH),
224
+ help=(
225
+ "Path to marketplace.json (defaults to <home>/.agents/plugins/marketplace.json). "
226
+ "Pass a repo-rooted marketplace path only when a repo/team plugin is intended."
227
+ ),
228
+ )
229
+ parser.add_argument(
230
+ "--marketplace-name",
231
+ help=(
232
+ "Marketplace name to seed into a new marketplace.json. Use this only when the default "
233
+ "'personal' marketplace name is already taken and you need a different new marketplace."
234
+ ),
235
+ )
236
+ parser.add_argument(
237
+ "--install-policy",
238
+ default=DEFAULT_INSTALL_POLICY,
239
+ choices=sorted(VALID_INSTALL_POLICIES),
240
+ help="Marketplace policy.installation value",
241
+ )
242
+ parser.add_argument(
243
+ "--auth-policy",
244
+ default=DEFAULT_AUTH_POLICY,
245
+ choices=sorted(VALID_AUTH_POLICIES),
246
+ help="Marketplace policy.authentication value",
247
+ )
248
+ parser.add_argument(
249
+ "--category",
250
+ default=DEFAULT_CATEGORY,
251
+ help="Marketplace category value",
252
+ )
253
+ parser.add_argument("--force", action="store_true", help="Overwrite existing files")
254
+ return parser.parse_args()
255
+
256
+
257
+ def main() -> None:
258
+ args = parse_args()
259
+ raw_plugin_name = args.plugin_name
260
+ plugin_name = normalize_plugin_name(raw_plugin_name)
261
+ if plugin_name != raw_plugin_name:
262
+ print(f"Note: Normalized plugin name from '{raw_plugin_name}' to '{plugin_name}'.")
263
+ validate_plugin_name(plugin_name)
264
+ marketplace_name = None
265
+ if args.marketplace_name is not None:
266
+ marketplace_name = args.marketplace_name.strip()
267
+ validate_marketplace_name(marketplace_name)
268
+
269
+ plugin_root = (Path(args.path).expanduser().resolve() / plugin_name)
270
+ plugin_root.mkdir(parents=True, exist_ok=True)
271
+
272
+ plugin_json_path = plugin_root / ".codex-plugin" / "plugin.json"
273
+ write_json(
274
+ plugin_json_path,
275
+ build_plugin_json(plugin_name, with_mcp=args.with_mcp, with_apps=args.with_apps),
276
+ args.force,
277
+ )
278
+
279
+ optional_directories = {
280
+ "skills": args.with_skills,
281
+ "hooks": args.with_hooks,
282
+ "scripts": args.with_scripts,
283
+ "assets": args.with_assets,
284
+ }
285
+ for folder, enabled in optional_directories.items():
286
+ if enabled:
287
+ (plugin_root / folder).mkdir(parents=True, exist_ok=True)
288
+
289
+ if args.with_mcp:
290
+ create_stub_file(
291
+ plugin_root / ".mcp.json",
292
+ {"mcpServers": {}},
293
+ args.force,
294
+ )
295
+
296
+ if args.with_apps:
297
+ create_stub_file(
298
+ plugin_root / ".app.json",
299
+ {
300
+ "apps": {},
301
+ },
302
+ args.force,
303
+ )
304
+
305
+ if args.with_marketplace:
306
+ marketplace_path = Path(args.marketplace_path).expanduser().resolve()
307
+ update_marketplace_json(
308
+ marketplace_path,
309
+ marketplace_name,
310
+ plugin_name,
311
+ args.install_policy,
312
+ args.auth_policy,
313
+ args.category,
314
+ args.force,
315
+ )
316
+
317
+ print(f"Created plugin scaffold: {plugin_root}")
318
+ print(f"plugin manifest: {plugin_json_path}")
319
+ if args.with_marketplace:
320
+ print(f"marketplace manifest: {marketplace_path}")
321
+
322
+
323
+ if __name__ == "__main__":
324
+ main()
@@ -0,0 +1,48 @@
1
+ #!/usr/bin/env python3
2
+ """Print the top-level marketplace name from any marketplace.json file."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import argparse
7
+ import json
8
+ import sys
9
+ from pathlib import Path
10
+
11
+
12
+ def default_marketplace_path() -> Path:
13
+ return Path.home() / ".agents" / "plugins" / "marketplace.json"
14
+
15
+
16
+ def parse_args() -> argparse.Namespace:
17
+ parser = argparse.ArgumentParser(
18
+ description=(
19
+ "Print the top-level marketplace name from marketplace.json. Defaults to the personal "
20
+ "marketplace path under the current home directory."
21
+ )
22
+ )
23
+ parser.add_argument(
24
+ "--marketplace-path",
25
+ default=str(default_marketplace_path()),
26
+ help="Path to marketplace.json",
27
+ )
28
+ return parser.parse_args()
29
+
30
+
31
+ def main() -> None:
32
+ args = parse_args()
33
+ marketplace_path = Path(args.marketplace_path).expanduser().resolve()
34
+ payload = json.loads(marketplace_path.read_text(encoding="utf-8"))
35
+ if not isinstance(payload, dict):
36
+ raise ValueError(f"{marketplace_path} must contain a JSON object.")
37
+ name = payload.get("name")
38
+ if not isinstance(name, str) or not name.strip():
39
+ raise ValueError(f"{marketplace_path} must contain a non-empty string 'name'.")
40
+ print(name.strip())
41
+
42
+
43
+ if __name__ == "__main__":
44
+ try:
45
+ main()
46
+ except Exception as err: # noqa: BLE001 - CLI should surface a single clear message.
47
+ print(str(err), file=sys.stderr)
48
+ raise SystemExit(1) from err
@@ -0,0 +1,78 @@
1
+ #!/usr/bin/env python3
2
+ """Rewrite a local plugin version to a single Codex cachebuster suffix."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import argparse
7
+ import json
8
+ import re
9
+ import sys
10
+ from datetime import datetime, timezone
11
+ from pathlib import Path
12
+
13
+
14
+ CACHEBUSTER_PREFIX = "codex"
15
+
16
+
17
+ def parse_args() -> argparse.Namespace:
18
+ parser = argparse.ArgumentParser(
19
+ description=(
20
+ "Rewrite a local plugin's version so it preserves everything before '+' and uses "
21
+ "a single +codex.<cachebuster> suffix."
22
+ )
23
+ )
24
+ parser.add_argument("plugin_path", help="Path to the plugin root directory")
25
+ parser.add_argument(
26
+ "--cachebuster",
27
+ help="Optional cachebuster token to embed in the plugin version",
28
+ )
29
+ return parser.parse_args()
30
+
31
+
32
+ def main() -> None:
33
+ args = parse_args()
34
+ plugin_root = Path(args.plugin_path).expanduser().resolve()
35
+ manifest_path = plugin_root / ".codex-plugin" / "plugin.json"
36
+ manifest = load_manifest(manifest_path)
37
+
38
+ version = manifest.get("version")
39
+ if not isinstance(version, str) or not version.strip():
40
+ raise ValueError(f"{manifest_path} must contain a non-empty string 'version'.")
41
+ cachebuster = sanitize_cachebuster(args.cachebuster or default_cachebuster())
42
+ next_version = with_cachebuster(version, cachebuster)
43
+ manifest["version"] = next_version
44
+ manifest_path.write_text(json.dumps(manifest, indent=2) + "\n", encoding="utf-8")
45
+
46
+ print(f"Updated plugin version: {version} -> {next_version}")
47
+
48
+
49
+ def load_manifest(manifest_path: Path) -> dict[str, object]:
50
+ if not manifest_path.is_file():
51
+ raise FileNotFoundError(f"missing manifest: {manifest_path}")
52
+ payload = json.loads(manifest_path.read_text(encoding="utf-8"))
53
+ if not isinstance(payload, dict):
54
+ raise ValueError(f"{manifest_path} must contain a JSON object.")
55
+ return payload
56
+ def sanitize_cachebuster(value: str) -> str:
57
+ sanitized = re.sub(r"[^a-z0-9-]+", "-", value.strip().lower())
58
+ sanitized = re.sub(r"-{2,}", "-", sanitized).strip("-")
59
+ if not sanitized:
60
+ raise ValueError("Cachebuster must contain at least one letter or digit.")
61
+ return sanitized
62
+
63
+
64
+ def default_cachebuster() -> str:
65
+ return datetime.now(timezone.utc).strftime("%Y%m%d%H%M%S")
66
+
67
+
68
+ def with_cachebuster(version: str, cachebuster: str) -> str:
69
+ version_prefix = version.split("+", 1)[0]
70
+ return f"{version_prefix}+{CACHEBUSTER_PREFIX}.{cachebuster}"
71
+
72
+
73
+ if __name__ == "__main__":
74
+ try:
75
+ main()
76
+ except Exception as err: # noqa: BLE001 - CLI should surface a single clear message.
77
+ print(str(err), file=sys.stderr)
78
+ raise SystemExit(1) from err