@postman/postman-plugin 0.2.2 → 0.4.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 (53) hide show
  1. package/README.md +58 -10
  2. package/dist/hosts/antigravity.js +47 -0
  3. package/dist/hosts/copilot.js +60 -0
  4. package/dist/hosts/index.js +3 -1
  5. package/dist/hosts/opencode-config.js +147 -0
  6. package/dist/hosts/opencode.js +315 -34
  7. package/dist/hosts/shared.js +10 -3
  8. package/dist/source.js +19 -5
  9. package/dist/system.js +20 -1
  10. package/hooks/session-start-context.md +1 -1
  11. package/mcp.pi.json +2 -2
  12. package/package.json +1 -1
  13. package/skills/ai-readiness/SKILL.md +40 -44
  14. package/skills/api-discovery/SKILL.md +64 -129
  15. package/skills/api-discovery/reference/orbit.md +17 -5
  16. package/skills/api-documentation/SKILL.md +38 -25
  17. package/skills/api-engineer/SKILL.md +78 -23
  18. package/skills/api-mocking/SKILL.md +117 -135
  19. package/skills/api-monitoring/SKILL.md +52 -129
  20. package/skills/api-simulation/SKILL.md +138 -0
  21. package/skills/api-simulation/reference/sim_yaml_schema.md +50 -0
  22. package/skills/api-testing/SKILL.md +48 -95
  23. package/skills/bootstrap/SKILL.md +123 -210
  24. package/skills/bootstrap/reference/cli_installation.md +4 -3
  25. package/skills/ci-integration/SKILL.md +53 -112
  26. package/skills/collection-schema-v3/SKILL.md +43 -204
  27. package/skills/collection-schema-v3/reference/http.md +150 -0
  28. package/skills/datasets/SKILL.md +89 -314
  29. package/skills/datasets/reference/spreadsheets.md +25 -0
  30. package/skills/flows/SKILL.md +57 -206
  31. package/skills/flows/reference/flow_cli_flags.md +3 -2
  32. package/skills/performance-testing/SKILL.md +46 -62
  33. package/skills/performance-testing/references/jmeter-import/README.md +113 -0
  34. package/skills/performance-testing/references/jmeter-import/import-report.md +52 -0
  35. package/skills/performance-testing/references/jmeter-import/mapping-table.md +97 -0
  36. package/skills/performance-testing/references/jmeter-import/postman-perf-capabilities.md +56 -0
  37. package/skills/performance-testing/references/jmeter-import/script-translation.md +98 -0
  38. package/skills/performance-testing/references/jmeter-import/scripts/groovy_to_js.py +83 -0
  39. package/skills/performance-testing/references/jmeter-import/scripts/jmx_to_postman.py +1433 -0
  40. package/skills/postman-mcp-server/SKILL.md +53 -59
  41. package/skills/postman-mcp-server/references/docs.md +0 -1
  42. package/skills/postman-mcp-server/references/learn.md +10 -9
  43. package/skills/postman-mcp-server/references/mock.md +0 -1
  44. package/skills/postman-mcp-server/references/search.md +0 -1
  45. package/skills/postman-mcp-server/references/security.md +0 -1
  46. package/skills/postman-mcp-server/references/setup.md +0 -1
  47. package/skills/postman-mcp-server/references/sync.md +0 -1
  48. package/skills/postman-mcp-server/references/test.md +0 -1
  49. package/skills/variables/SKILL.md +63 -0
  50. package/skills/webhook-testing/SKILL.md +103 -0
  51. package/skills/webhook-testing/reference/forwarding.md +10 -0
  52. package/skills/webhook-testing/reference/outbound_webhooks.md +25 -0
  53. package/skills/webhook-testing/reference/webhook_scripts.md +163 -0
package/README.md CHANGED
@@ -24,12 +24,22 @@ Install Postman in every compatible coding agent detected on your machine:
24
24
  npx @postman/postman-plugin
25
25
  ```
26
26
 
27
- One command configures **Claude Code, Codex, Cursor, Factory Droid, Kimi Code,
28
- OpenCode and Pi**. Run it again to update, `status` to see what's installed,
27
+ One command configures **Antigravity, Claude Code, Codex, GitHub Copilot CLI, Cursor,
28
+ Factory Droid, Kimi Code, OpenCode and Pi**. Run it again to update, `status` to see what's installed,
29
29
  and `remove` to uninstall; `--agent <id>` limits any of them to one agent.
30
30
 
31
31
  You can also use the following commands to install individually:
32
32
 
33
+ ### Antigravity
34
+
35
+ ```bash
36
+ agy plugin install https://github.com/postmanlabs/postman-plugin
37
+ ```
38
+
39
+ Run the same command again to update; `agy plugin uninstall postman` removes it.
40
+ In the Antigravity app, **Customizations → Plugins → + Install from URL** takes
41
+ the same URL.
42
+
33
43
  ### Claude Code
34
44
 
35
45
  [View Postman on Claude Plugins](https://claude.com/plugins/postman)
@@ -60,6 +70,16 @@ codex plugin add postman@postman
60
70
  /add-plugin postman
61
71
  ```
62
72
 
73
+ ### GitHub Copilot CLI
74
+
75
+ ```bash
76
+ copilot plugin marketplace add postmanlabs/postman-plugin
77
+ copilot plugin install postman@postman
78
+ ```
79
+
80
+ `copilot plugin marketplace update postman`, then `copilot plugin update postman`, updates it. Sign in to Postman's MCP server
81
+ with `/mcp` inside a Copilot CLI session.
82
+
63
83
  ### Factory Droid
64
84
 
65
85
  ```bash
@@ -85,8 +105,14 @@ Postman's MCP server with `/mcp-config login plugin-postman:postman`.
85
105
 
86
106
  ### OpenCode
87
107
 
88
- OpenCode loads Postman from a clone of this repository and a one-line loader
89
- file. [opencode/README.md](https://github.com/postmanlabs/postman-plugin/blob/main/opencode/README.md#install) has the commands.
108
+ ```bash
109
+ opencode plugin add github:postmanlabs/postman-plugin
110
+ ```
111
+
112
+ That is OpenCode 2.0.4 or later. On OpenCode 1.14.33 or later the command is
113
+ `opencode plugin --global github:postmanlabs/postman-plugin`.
114
+ [opencode/README.md](https://github.com/postmanlabs/postman-plugin/blob/main/opencode/README.md#install) has how to update and remove
115
+ it, and the older clone-and-loader install.
90
116
 
91
117
  ### Pi
92
118
 
@@ -166,17 +192,39 @@ test consumers without waiting for the real service to be ready or available.
166
192
 
167
193
  The mock stays local until you choose to push and deploy it. When teammates or
168
194
  external systems need access, the same mock can become a durable hosted URL
169
- without rebuilding it in another tool.
195
+ without rebuilding it in another tool. Pushed to the service's own workspace,
196
+ it's the mock other teams' simulations pull first.
197
+
198
+ ### Develop against simulated dependencies
199
+
200
+ The [`api-simulation`](https://github.com/postmanlabs/postman-plugin/blob/main/skills/api-simulation/) skill runs your code (a service
201
+ you're changing, a frontend, a test suite or a demo) for real against stand-ins
202
+ for the HTTP services it calls, served together on one port from one
203
+ `.sim.yaml`. Each stand-in is the owning team's mock when they publish one.
204
+ It reuses an existing `.sim.yaml`, proves a change with a request through the
205
+ changed path, and answers "what happens if X goes down" by running each failure.
206
+ Pushing the simulation is optional and needs your approval. It needs Postman CLI
207
+ 1.70.0 or later:
208
+
209
+ ```bash
210
+ postman simulation run postman/simulations/orders-dev.sim.yaml --port 4900
211
+ ```
212
+
213
+ The service under test is never mocked. It runs for real, so a passing run
214
+ shows the change works against what the mocks model; it doesn't replace a run
215
+ against the real upstreams. The simulation can also run in CI, and any
216
+ dependency can take injected latency, error, rate-limit, or chaos conditions to
217
+ exercise failure paths.
170
218
 
171
219
 
172
220
  ## How Postman builds context
173
221
 
174
222
  Work done in a Postman folder produces artifacts: collection runs, lint
175
223
  results, request history, mock deployments. When that folder is connected to a
176
- Postman workspace, after `postman init`, those artifacts are
177
- published to it as they are produced. The workspace is
178
- where a teammate, a reviewer, or another agent can see what the agent actually
179
- did.
224
+ Postman workspace after `postman init`, those artifacts are published to it as
225
+ they are produced. The workspace persists that context for future work and
226
+ enables teammates, reviewers, and other agents to share and build on what the
227
+ agent did.
180
228
 
181
229
  What each command contributes to the connected workspace:
182
230
 
@@ -185,7 +233,7 @@ What each command contributes to the connected workspace:
185
233
  | `postman collection run` | Run results and run history |
186
234
  | `postman application test` | Test run results |
187
235
  | `postman spec lint` | Lint results, including violation counts and pass/fail |
188
- | `postman workspace push` | The local workspace contents |
236
+ | `postman workspace push` | Local workspace contents, persisted as context for future work and sharing with others |
189
237
  | `postman runner start` | Runner processing and health activity |
190
238
  | `postman flows run` | Flow run results |
191
239
  | `postman request` | Agent's API interaction activity |
@@ -0,0 +1,47 @@
1
+ import { REPO } from '../source.js';
2
+ import { guard, mustProbeJson, mustRun, parseJson } from './shared.js';
3
+ import { result } from './types.js';
4
+ // `agy plugin install` clones a git URL into ~/.gemini/config/plugins/<name>. It has no update
5
+ // command; installing again replaces the copy in place, so a re-run is the update.
6
+ const PLUGIN_NAME = 'postman', SOURCE = `https://github.com/${REPO}`, NEXT = 'Restart Antigravity for the change to take effect.';
7
+ const isOurs = (plugin) => plugin.name === PLUGIN_NAME;
8
+ export const antigravity = {
9
+ id: 'antigravity',
10
+ name: 'Antigravity',
11
+ route: '.antigravity-plugin',
12
+ async detect(system) {
13
+ return (await system.which('agy')) !== null;
14
+ },
15
+ async status(system) {
16
+ const exec = await system.probe('agy', ['plugin', 'list', '--json']), plugins = exec.code === 0 ? parseJson(exec.stdout) : null;
17
+ if (!Array.isArray(plugins)) {
18
+ return { installed: null, detail: 'could not read `agy plugin list --json`', notes: [] };
19
+ }
20
+ const installed = plugins.find(isOurs);
21
+ if (!installed) {
22
+ return { installed: false, detail: 'not installed', notes: [] };
23
+ }
24
+ return {
25
+ installed: true,
26
+ detail: `${PLUGIN_NAME} ${installed.version ?? ''}`.trim(),
27
+ notes: installed.enabled === false ? ['installed but disabled; run `agy plugin enable postman`'] : []
28
+ };
29
+ },
30
+ install(system) {
31
+ return guard(async () => {
32
+ const plugins = await mustProbeJson(system, 'agy', ['plugin', 'list', '--json']), current = plugins.some(isOurs);
33
+ await mustRun(system, 'agy', ['plugin', 'install', SOURCE]);
34
+ return result('done', `${current ? 'updated' : 'installed'} ${PLUGIN_NAME}`, NEXT);
35
+ });
36
+ },
37
+ remove(system) {
38
+ return guard(async () => {
39
+ const plugins = await mustProbeJson(system, 'agy', ['plugin', 'list', '--json']);
40
+ if (!plugins.some(isOurs)) {
41
+ return result('skipped', 'not installed');
42
+ }
43
+ await mustRun(system, 'agy', ['plugin', 'uninstall', PLUGIN_NAME]);
44
+ return result('done', `uninstalled ${PLUGIN_NAME}`, NEXT);
45
+ });
46
+ }
47
+ };
@@ -0,0 +1,60 @@
1
+ import { REPO } from '../source.js';
2
+ import { guard, mustProbeJson, mustRun, parseJson } from './shared.js';
3
+ import { result } from './types.js';
4
+ // Copilot CLI is deprecating direct installs (repos, URLs, local paths), so this installs from this
5
+ // repo as a marketplace, which Copilot reads from `.claude-plugin/marketplace.json`. Once Postman is
6
+ // listed in awesome-copilot, which Copilot ships registered, `postman@awesome-copilot` replaces it.
7
+ const PLUGIN_NAME = 'postman', MARKETPLACE = 'postman', PLUGIN_ID = `${PLUGIN_NAME}@${MARKETPLACE}`, NEXT = 'Restart Copilot CLI (or run /restart) for the change to take effect.';
8
+ const isOurs = (plugin) => plugin.name === PLUGIN_NAME,
9
+ // A direct install has no marketplace; it would load the same skills a second time.
10
+ isDirect = (plugin) => isOurs(plugin) && !plugin.marketplace;
11
+ export const copilot = {
12
+ id: 'copilot',
13
+ name: 'GitHub Copilot CLI',
14
+ route: '.github/plugin',
15
+ async detect(system) {
16
+ return (await system.which('copilot')) !== null;
17
+ },
18
+ async status(system) {
19
+ const exec = await system.probe('copilot', ['plugin', 'list', '--json']), plugins = exec.code === 0 ? parseJson(exec.stdout) : null;
20
+ if (!Array.isArray(plugins)) {
21
+ return { installed: null, detail: 'could not read `copilot plugin list --json`', notes: [] };
22
+ }
23
+ const installed = plugins.find(isOurs);
24
+ if (!installed) {
25
+ return { installed: false, detail: 'not installed', notes: [] };
26
+ }
27
+ return {
28
+ installed: true,
29
+ detail: `${PLUGIN_NAME} ${installed.version ?? ''}`.trim(),
30
+ notes: installed.enabled === false ? ['installed but disabled; run `copilot plugin enable postman`'] : []
31
+ };
32
+ },
33
+ install(system) {
34
+ return guard(async () => {
35
+ const plugins = await mustProbeJson(system, 'copilot', ['plugin', 'list', '--json']), marketplaces = await mustProbeJson(system, 'copilot', ['plugin', 'marketplace', 'list', '--json']), current = plugins.some((plugin) => isOurs(plugin) && plugin.marketplace === MARKETPLACE);
36
+ if (marketplaces.some((marketplace) => marketplace.name === MARKETPLACE)) {
37
+ await mustRun(system, 'copilot', ['plugin', 'marketplace', 'update', MARKETPLACE]);
38
+ }
39
+ else {
40
+ await mustRun(system, 'copilot', ['plugin', 'marketplace', 'add', REPO]);
41
+ }
42
+ // The same name can't be installed twice, so a direct install goes first.
43
+ if (plugins.some(isDirect)) {
44
+ await mustRun(system, 'copilot', ['plugin', 'uninstall', PLUGIN_NAME]);
45
+ }
46
+ await mustRun(system, 'copilot', current ? ['plugin', 'update', PLUGIN_NAME] : ['plugin', 'install', PLUGIN_ID]);
47
+ return result('done', `${current ? 'updated' : 'installed'} ${PLUGIN_ID}`, NEXT);
48
+ });
49
+ },
50
+ remove(system) {
51
+ return guard(async () => {
52
+ const plugins = await mustProbeJson(system, 'copilot', ['plugin', 'list', '--json']);
53
+ if (!plugins.some(isOurs)) {
54
+ return result('skipped', 'not installed');
55
+ }
56
+ await mustRun(system, 'copilot', ['plugin', 'uninstall', PLUGIN_NAME]);
57
+ return result('done', `uninstalled ${PLUGIN_NAME}`, NEXT);
58
+ });
59
+ }
60
+ };
@@ -1,8 +1,10 @@
1
+ import { antigravity } from './antigravity.js';
1
2
  import { claudeCode } from './claude-code.js';
2
3
  import { codex } from './codex.js';
4
+ import { copilot } from './copilot.js';
3
5
  import { cursor } from './cursor.js';
4
6
  import { factory } from './factory.js';
5
7
  import { kimi } from './kimi.js';
6
8
  import { opencode } from './opencode.js';
7
9
  import { pi } from './pi.js';
8
- export const HOSTS = [claudeCode, codex, cursor, factory, kimi, opencode, pi];
10
+ export const HOSTS = [antigravity, claudeCode, codex, copilot, cursor, factory, kimi, opencode, pi];
@@ -0,0 +1,147 @@
1
+ // OpenCode's config is JSONC: comments and trailing commas are allowed, and users edit it by hand.
2
+ // Reading it and removing one array entry from it must therefore leave everything else as written.
3
+ const PUNCTUATION = '{}[]:,', WHITESPACE = /\s/, WHITESPACE_ONLY = /^\s*$/;
4
+ function tokenize(text) {
5
+ const tokens = [];
6
+ let index = text.charCodeAt(0) === 0xFEFF ? 1 : 0;
7
+ while (index < text.length) {
8
+ const char = text[index];
9
+ if (WHITESPACE.test(char)) {
10
+ index += 1;
11
+ }
12
+ else if (text.startsWith('//', index)) {
13
+ // A line comment ends at either line terminator; a CR-only file has no \n at all.
14
+ const lineEnd = text.slice(index).search(/[\r\n]/);
15
+ index = lineEnd === -1 ? text.length : index + lineEnd + 1;
16
+ }
17
+ else if (text.startsWith('/*', index)) {
18
+ const close = text.indexOf('*/', index + 2);
19
+ if (close === -1) {
20
+ // An unterminated comment is not valid JSONC; keep it as a token JSON.parse will reject.
21
+ tokens.push({ kind: 'other', start: index, end: text.length, text: text.slice(index) });
22
+ break;
23
+ }
24
+ index = close + 2;
25
+ }
26
+ else if (char === '"') {
27
+ let end = index + 1;
28
+ while (end < text.length && text[end] !== '"') {
29
+ end += text[end] === '\\' ? 2 : 1;
30
+ }
31
+ tokens.push({ kind: 'string', start: index, end: end + 1, text: text.slice(index, end + 1) });
32
+ index = end + 1;
33
+ }
34
+ else if (PUNCTUATION.includes(char)) {
35
+ tokens.push({ kind: 'punct', start: index, end: index + 1, text: char });
36
+ index += 1;
37
+ }
38
+ else {
39
+ let end = index;
40
+ while (end < text.length && !WHITESPACE.test(text[end]) && !PUNCTUATION.includes(text[end]) && text[end] !== '"' && !text.startsWith('//', end) && !text.startsWith('/*', end)) {
41
+ end += 1;
42
+ }
43
+ tokens.push({ kind: 'other', start: index, end, text: text.slice(index, end) });
44
+ index = end;
45
+ }
46
+ }
47
+ return tokens;
48
+ }
49
+ /** Parses JSONC; `null` when it is not valid even after comments and trailing commas are dropped. */
50
+ export function parseJsonc(text) {
51
+ const tokens = tokenize(text);
52
+ let json = '';
53
+ tokens.forEach((token, at) => {
54
+ const next = tokens[at + 1];
55
+ if (token.text === ',' && next && (next.text === ']' || next.text === '}')) {
56
+ return;
57
+ }
58
+ json += `${token.text} `;
59
+ });
60
+ try {
61
+ return JSON.parse(json || 'null');
62
+ }
63
+ catch {
64
+ return null;
65
+ }
66
+ }
67
+ /**
68
+ * The direct string elements of the root object's array under `key`, or `null` when there is no such array or the root
69
+ * object has `key` more than once: JSON.parse keeps the last one, so editing any of them could leave the one that counts.
70
+ */
71
+ function stringElements(tokens, key) {
72
+ let depth = 0, found = null, occurrences = 0;
73
+ for (let at = 0; at < tokens.length; at += 1) {
74
+ const token = tokens[at];
75
+ if (token.kind === 'punct' && '{['.includes(token.text)) {
76
+ depth += 1;
77
+ }
78
+ else if (token.kind === 'punct' && '}]'.includes(token.text)) {
79
+ depth -= 1;
80
+ }
81
+ else if (depth === 1 && token.kind === 'string' && tokens[at + 1]?.text === ':') {
82
+ let value;
83
+ try {
84
+ value = JSON.parse(token.text);
85
+ }
86
+ catch {
87
+ continue;
88
+ }
89
+ if (value !== key) {
90
+ continue;
91
+ }
92
+ occurrences += 1;
93
+ if (tokens[at + 2]?.text !== '[') {
94
+ continue;
95
+ }
96
+ const elements = [];
97
+ let nested = 1;
98
+ for (let inner = at + 3; inner < tokens.length && nested > 0; inner += 1) {
99
+ const item = tokens[inner];
100
+ if (item.kind === 'punct' && '{['.includes(item.text)) {
101
+ nested += 1;
102
+ }
103
+ else if (item.kind === 'punct' && '}]'.includes(item.text)) {
104
+ nested -= 1;
105
+ }
106
+ else if (nested === 1 && item.kind === 'string') {
107
+ elements.push({
108
+ start: item.start,
109
+ end: item.end,
110
+ value: JSON.parse(item.text),
111
+ before: tokens[inner - 1].text === ',' ? tokens[inner - 1] : null,
112
+ after: tokens[inner + 1]?.text === ',' ? tokens[inner + 1] : null
113
+ });
114
+ }
115
+ }
116
+ found ??= elements;
117
+ }
118
+ }
119
+ return occurrences === 1 ? found : null;
120
+ }
121
+ /** `text` without the first `value` in the array under `key`, comments and layout intact; `null` if it is not there or the result would not parse. */
122
+ export function withoutArrayString(text, key, value) {
123
+ const element = stringElements(tokenize(text), key)?.find((entry) => entry.value === value);
124
+ if (!element) {
125
+ return null;
126
+ }
127
+ let start = element.start, end = element.end;
128
+ if (element.after) {
129
+ end = element.after.end;
130
+ while (WHITESPACE.test(text[end] ?? '')) {
131
+ end += 1;
132
+ }
133
+ }
134
+ else if (element.before && WHITESPACE_ONLY.test(text.slice(element.before.end, element.start))) {
135
+ start = element.before.start;
136
+ }
137
+ // A comment between the comma and the last element belongs to the element before it, so only the comma goes.
138
+ else if (element.before) {
139
+ while (/[ \t]/.test(text[start - 1] ?? '')) {
140
+ start -= 1;
141
+ }
142
+ const edited = text.slice(0, element.before.start) + text.slice(element.before.end, start) + text.slice(end);
143
+ return parseJsonc(edited) === null ? null : edited;
144
+ }
145
+ const edited = text.slice(0, start) + text.slice(end);
146
+ return parseJsonc(edited) === null ? null : edited;
147
+ }