@agorio/plugin-procurement 0.1.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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Agorio
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,50 @@
1
+ # @agorio/plugin-procurement
2
+
3
+ B2B procurement tooling for [Agorio](https://www.npmjs.com/package/@agorio/sdk) AI shopping agents.
4
+
5
+ - **PO# tracking** — assign a purchase-order number to every cart, optionally gate `submit_payment` on its presence
6
+ - **Vendor management** — resolve merchant domain → vendor record (payment terms, tax category, preferred-supplier flag)
7
+ - **Expense categorization** — tag carts with one of your configured expense categories
8
+ - **Audit-friendly events** — emits a `procurement_completed` event on successful checkout so the [`@agorio/plugin-audit-trail`](https://www.npmjs.com/package/@agorio/plugin-audit-trail) picks up PO# + vendor + category for free
9
+
10
+ ```bash
11
+ npm install @agorio/sdk @agorio/plugin-procurement
12
+ ```
13
+
14
+ ```ts
15
+ import { ShoppingAgent } from '@agorio/sdk';
16
+ import { createProcurementPlugin } from '@agorio/plugin-procurement';
17
+
18
+ const procurement = createProcurementPlugin({
19
+ vendors: [
20
+ { id: 'acme', name: 'Acme Office Supplies', domain: 'acme.com', paymentTerms: 'NET-30', taxCategory: 'office', preferred: true },
21
+ { id: 'staples', name: 'Staples', domain: 'staples.com', paymentTerms: 'NET-15', taxCategory: 'office' },
22
+ ],
23
+ expenseCategories: ['office-supplies', 'it-equipment', 'furniture'],
24
+ poNumberPrefix: 'PO-2026',
25
+ poNumberStrategy: 'sequential',
26
+ requirePoOnCheckout: true,
27
+ });
28
+
29
+ const agent = new ShoppingAgent({ llm, plugins: [procurement] });
30
+ ```
31
+
32
+ ## Configuration
33
+
34
+ | Key | Type | Description |
35
+ | ---------------------- | -------------------------------------- | --- |
36
+ | `vendors` | `VendorConfig[]` | Known vendors, indexed by domain. |
37
+ | `expenseCategories` | `string[]` | Allowed values for `categorize_expense`. |
38
+ | `poNumberPrefix` | `string` | Optional prefix on generated PO numbers (default `'PO'`). |
39
+ | `poNumberStrategy` | `'sequential' \| 'uuid' \| () => string` | How to generate PO numbers (default `'sequential'`). |
40
+ | `requirePoOnCheckout` | `boolean` | Block `submit_payment` if the current cart has no PO# (default `false`). |
41
+
42
+ ## Tools exposed to the LLM
43
+
44
+ - `assign_po_number` — `{ }` → `{ poNumber }`
45
+ - `lookup_vendor` — `{ domain }` → `{ vendor }` (or `{ error }`)
46
+ - `categorize_expense` — `{ category }` → `{ assigned: true, category }`
47
+
48
+ ## License
49
+
50
+ MIT — same as the rest of Agorio's plugins.
@@ -0,0 +1,30 @@
1
+ import type { EnterprisePlugin, PluginManifest } from '@agorio/sdk';
2
+ export declare const PLUGIN_MANIFEST: PluginManifest;
3
+ export interface VendorConfig {
4
+ id: string;
5
+ name: string;
6
+ domain: string;
7
+ paymentTerms?: string;
8
+ taxCategory?: string;
9
+ preferred?: boolean;
10
+ metadata?: Record<string, unknown>;
11
+ }
12
+ export type PoNumberStrategy = 'sequential' | 'uuid' | (() => string);
13
+ export interface ProcurementConfig {
14
+ vendors?: VendorConfig[];
15
+ expenseCategories?: string[];
16
+ poNumberPrefix?: string;
17
+ poNumberStrategy?: PoNumberStrategy;
18
+ requirePoOnCheckout?: boolean;
19
+ onProcurementCompleted?: (event: ProcurementCompletedEvent) => void;
20
+ }
21
+ export interface ProcurementCompletedEvent {
22
+ poNumber: string;
23
+ vendorId: string | null;
24
+ category: string | null;
25
+ amount: number;
26
+ currency: string;
27
+ merchant: string | null;
28
+ timestamp: number;
29
+ }
30
+ export declare function createProcurementPlugin(config?: ProcurementConfig): EnterprisePlugin;
package/dist/index.js ADDED
@@ -0,0 +1,166 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ export const PLUGIN_MANIFEST = {
3
+ version: '0.1.0',
4
+ author: 'Agorio',
5
+ category: 'governance',
6
+ tier: 'pro',
7
+ };
8
+ export function createProcurementPlugin(config = {}) {
9
+ const vendorsByDomain = new Map();
10
+ for (const v of config.vendors ?? []) {
11
+ vendorsByDomain.set(normalizeDomain(v.domain), v);
12
+ }
13
+ const expenseCategorySet = new Set(config.expenseCategories ?? []);
14
+ const prefix = config.poNumberPrefix ?? 'PO';
15
+ const strategy = config.poNumberStrategy ?? 'sequential';
16
+ let sequentialCounter = 0;
17
+ /** Cart tags keyed by `<merchant_domain>` (one in-flight tag per merchant). */
18
+ const cartTags = new Map();
19
+ let pluginCtx;
20
+ function getOrCreateTag(merchant) {
21
+ const key = merchant ?? '__none__';
22
+ let tag = cartTags.get(key);
23
+ if (!tag) {
24
+ tag = { poNumber: null, vendorId: null, category: null };
25
+ cartTags.set(key, tag);
26
+ }
27
+ return tag;
28
+ }
29
+ function generatePoNumber() {
30
+ if (typeof strategy === 'function')
31
+ return strategy();
32
+ if (strategy === 'uuid')
33
+ return `${prefix}-${randomUUID()}`;
34
+ sequentialCounter++;
35
+ return `${prefix}-${String(sequentialCounter).padStart(6, '0')}`;
36
+ }
37
+ return {
38
+ name: 'procurement',
39
+ description: 'Procurement tooling — assign purchase-order numbers, look up vendor info, and categorize the active cart\'s expense. Use `assign_po_number` before checkout if PO numbers are required.',
40
+ parameters: {
41
+ type: 'object',
42
+ properties: {
43
+ action: {
44
+ type: 'string',
45
+ enum: ['assign_po_number', 'lookup_vendor', 'categorize_expense'],
46
+ description: 'Procurement action to perform.',
47
+ },
48
+ domain: {
49
+ type: 'string',
50
+ description: 'Merchant domain (for `lookup_vendor`).',
51
+ },
52
+ category: {
53
+ type: 'string',
54
+ description: 'Expense category (for `categorize_expense`). Must be one of the configured categories.',
55
+ },
56
+ },
57
+ required: ['action'],
58
+ },
59
+ manifest: PLUGIN_MANIFEST,
60
+ handler(args) {
61
+ const action = String(args.action);
62
+ switch (action) {
63
+ case 'assign_po_number': {
64
+ const merchant = pluginCtx?.getActiveMerchant() ?? null;
65
+ const tag = getOrCreateTag(merchant);
66
+ tag.poNumber = generatePoNumber();
67
+ if (merchant) {
68
+ const vendor = vendorsByDomain.get(normalizeDomain(merchant));
69
+ if (vendor)
70
+ tag.vendorId = vendor.id;
71
+ }
72
+ return { poNumber: tag.poNumber, vendorId: tag.vendorId };
73
+ }
74
+ case 'lookup_vendor': {
75
+ const domain = String(args.domain ?? pluginCtx?.getActiveMerchant() ?? '');
76
+ if (!domain)
77
+ return { error: 'lookup_vendor: `domain` is required (no active merchant either)' };
78
+ const vendor = vendorsByDomain.get(normalizeDomain(domain));
79
+ if (!vendor)
80
+ return { error: `Unknown vendor for domain: ${domain}` };
81
+ return { vendor };
82
+ }
83
+ case 'categorize_expense': {
84
+ const category = String(args.category ?? '');
85
+ if (!category)
86
+ return { error: 'categorize_expense: `category` is required' };
87
+ if (expenseCategorySet.size > 0 && !expenseCategorySet.has(category)) {
88
+ return {
89
+ error: `Unknown expense category "${category}". Allowed: ${[...expenseCategorySet].join(', ')}`,
90
+ };
91
+ }
92
+ const merchant = pluginCtx?.getActiveMerchant() ?? null;
93
+ const tag = getOrCreateTag(merchant);
94
+ tag.category = category;
95
+ return { assigned: true, category };
96
+ }
97
+ default:
98
+ return { error: `Unknown procurement action: ${action}` };
99
+ }
100
+ },
101
+ onInit(context) {
102
+ pluginCtx = context;
103
+ },
104
+ onBeforeToolCall(toolName, _args, context) {
105
+ if (toolName !== 'submit_payment')
106
+ return { allow: true };
107
+ if (!config.requirePoOnCheckout)
108
+ return { allow: true };
109
+ const merchant = context.getActiveMerchant();
110
+ const tag = cartTags.get(merchant ?? '__none__');
111
+ if (!tag || !tag.poNumber) {
112
+ return {
113
+ allow: false,
114
+ reason: 'PO# required for this transaction. Call the `procurement` tool with action `assign_po_number` first.',
115
+ };
116
+ }
117
+ return { allow: true };
118
+ },
119
+ onAfterToolCall(toolName, _args, result, context) {
120
+ if (toolName !== 'submit_payment')
121
+ return;
122
+ if (!result || typeof result !== 'object' || !('orderId' in result))
123
+ return;
124
+ const merchant = context.getActiveMerchant();
125
+ const tag = cartTags.get(merchant ?? '__none__');
126
+ const cart = context.getCart();
127
+ const amount = parseFloat(cart.subtotal.amount);
128
+ const event = {
129
+ poNumber: tag?.poNumber ?? '',
130
+ vendorId: tag?.vendorId ?? null,
131
+ category: tag?.category ?? null,
132
+ amount,
133
+ currency: cart.subtotal.currency,
134
+ merchant,
135
+ timestamp: Date.now(),
136
+ };
137
+ config.onProcurementCompleted?.(event);
138
+ // Reset the tag after a successful checkout so the next purchase on the
139
+ // same merchant starts fresh.
140
+ cartTags.delete(merchant ?? '__none__');
141
+ },
142
+ getState() {
143
+ return {
144
+ cartTags: Array.from(cartTags.entries()).map(([merchant, tag]) => ({
145
+ merchant,
146
+ ...tag,
147
+ })),
148
+ sequentialCounter,
149
+ };
150
+ },
151
+ hydrate(state) {
152
+ cartTags.clear();
153
+ const tags = Array.isArray(state.cartTags) ? state.cartTags : [];
154
+ for (const entry of tags) {
155
+ const { merchant, ...rest } = entry;
156
+ cartTags.set(merchant, rest);
157
+ }
158
+ if (typeof state.sequentialCounter === 'number') {
159
+ sequentialCounter = state.sequentialCounter;
160
+ }
161
+ },
162
+ };
163
+ }
164
+ function normalizeDomain(d) {
165
+ return d.replace(/^https?:\/\//, '').replace(/\/+$/, '').toLowerCase();
166
+ }
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "@agorio/plugin-procurement",
3
+ "version": "0.1.0",
4
+ "description": "B2B procurement plugin for Agorio SDK — PO# tracking, vendor management, and expense categorization for AI shopping agents",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "files": [
9
+ "dist"
10
+ ],
11
+ "scripts": {
12
+ "build": "tsc",
13
+ "typecheck": "tsc --noEmit",
14
+ "test": "vitest run",
15
+ "prepublishOnly": "npm run build"
16
+ },
17
+ "peerDependencies": {
18
+ "@agorio/sdk": "^0.7.0"
19
+ },
20
+ "devDependencies": {
21
+ "@agorio/sdk": "file:../..",
22
+ "@types/node": "^22.9.0",
23
+ "typescript": "^5.6.3",
24
+ "vitest": "^4.0.17"
25
+ },
26
+ "keywords": [
27
+ "agorio",
28
+ "ucp",
29
+ "acp",
30
+ "ai-agent",
31
+ "shopping-agent",
32
+ "procurement",
33
+ "b2b",
34
+ "purchase-order",
35
+ "vendor",
36
+ "expense",
37
+ "enterprise"
38
+ ],
39
+ "author": "Agorio",
40
+ "license": "MIT",
41
+ "repository": {
42
+ "type": "git",
43
+ "url": "https://github.com/Nolpak14/agorio",
44
+ "directory": "plugins/procurement"
45
+ }
46
+ }