zapier-platform-cli 16.2.0 → 16.3.1
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/oclif.manifest.json +91 -83
- package/package.json +1 -1
- package/src/generators/index.js +1 -0
- package/src/generators/pull.js +4 -1
- package/src/generators/templates/openai/README.md +3 -0
- package/src/generators/templates/openai/authentication.js +46 -0
- package/src/generators/templates/openai/constants.js +10 -0
- package/src/generators/templates/openai/creates/chat_completion.js +164 -0
- package/src/generators/templates/openai/creates/index.js +7 -0
- package/src/generators/templates/openai/dynamic_dropdowns/index.js +7 -0
- package/src/generators/templates/openai/dynamic_dropdowns/list_models.js +24 -0
- package/src/generators/templates/openai/index.js +33 -0
- package/src/generators/templates/openai/middleware.js +50 -0
- package/src/generators/templates/openai/samples/chat.json +29 -0
- package/src/generators/templates/openai/test/authentication.test.js +66 -0
- package/src/generators/templates/openai/test/chat_completion.test.js +150 -0
- package/src/generators/templates/openai/test/list_models.test.js +51 -0
- package/src/oclif/commands/invoke.js +1 -1
- package/src/oclif/commands/push.js +10 -1
- package/src/utils/api.js +38 -10
package/oclif.manifest.json
CHANGED
|
@@ -349,6 +349,7 @@
|
|
|
349
349
|
"minimal",
|
|
350
350
|
"oauth1-trello",
|
|
351
351
|
"oauth2",
|
|
352
|
+
"openai",
|
|
352
353
|
"search-or-create",
|
|
353
354
|
"session-auth",
|
|
354
355
|
"typescript"
|
|
@@ -1061,6 +1062,13 @@
|
|
|
1061
1062
|
"name": "invokedFromAnotherCommand",
|
|
1062
1063
|
"allowNo": false,
|
|
1063
1064
|
"type": "boolean"
|
|
1065
|
+
},
|
|
1066
|
+
"overwrite-partner-changes": {
|
|
1067
|
+
"description": "(Internal Use Only) Allows Zapier Staff to push changes to integrations in certain situations.",
|
|
1068
|
+
"hidden": true,
|
|
1069
|
+
"name": "overwrite-partner-changes",
|
|
1070
|
+
"allowNo": false,
|
|
1071
|
+
"type": "boolean"
|
|
1064
1072
|
}
|
|
1065
1073
|
},
|
|
1066
1074
|
"hasDynamicHelp": false,
|
|
@@ -1696,88 +1704,6 @@
|
|
|
1696
1704
|
"list.js"
|
|
1697
1705
|
]
|
|
1698
1706
|
},
|
|
1699
|
-
"delete:integration": {
|
|
1700
|
-
"aliases": [
|
|
1701
|
-
"delete:app"
|
|
1702
|
-
],
|
|
1703
|
-
"args": {},
|
|
1704
|
-
"description": "Delete your integration (including all versions).\n\nThis only works if there are no active users or Zaps on any version. If you only want to delete certain versions, use the `zapier delete:version` command instead. It's unlikely that you'll be able to run this on an app that you've pushed publicly, since there are usually still users.",
|
|
1705
|
-
"flags": {
|
|
1706
|
-
"debug": {
|
|
1707
|
-
"char": "d",
|
|
1708
|
-
"description": "Show extra debugging output.",
|
|
1709
|
-
"name": "debug",
|
|
1710
|
-
"allowNo": false,
|
|
1711
|
-
"type": "boolean"
|
|
1712
|
-
},
|
|
1713
|
-
"invokedFromAnotherCommand": {
|
|
1714
|
-
"hidden": true,
|
|
1715
|
-
"name": "invokedFromAnotherCommand",
|
|
1716
|
-
"allowNo": false,
|
|
1717
|
-
"type": "boolean"
|
|
1718
|
-
}
|
|
1719
|
-
},
|
|
1720
|
-
"hasDynamicHelp": false,
|
|
1721
|
-
"hiddenAliases": [],
|
|
1722
|
-
"id": "delete:integration",
|
|
1723
|
-
"pluginAlias": "zapier-platform-cli",
|
|
1724
|
-
"pluginName": "zapier-platform-cli",
|
|
1725
|
-
"pluginType": "core",
|
|
1726
|
-
"strict": true,
|
|
1727
|
-
"enableJsonFlag": false,
|
|
1728
|
-
"skipValidInstallCheck": true,
|
|
1729
|
-
"isESM": false,
|
|
1730
|
-
"relativePath": [
|
|
1731
|
-
"src",
|
|
1732
|
-
"oclif",
|
|
1733
|
-
"commands",
|
|
1734
|
-
"delete",
|
|
1735
|
-
"integration.js"
|
|
1736
|
-
]
|
|
1737
|
-
},
|
|
1738
|
-
"delete:version": {
|
|
1739
|
-
"aliases": [],
|
|
1740
|
-
"args": {
|
|
1741
|
-
"version": {
|
|
1742
|
-
"description": "Specify the version to delete. It must have no users or Zaps.",
|
|
1743
|
-
"name": "version",
|
|
1744
|
-
"required": true
|
|
1745
|
-
}
|
|
1746
|
-
},
|
|
1747
|
-
"description": "Delete a specific version of your integration.\n\nThis only works if there are no users or Zaps on that version. You will probably need to have run `zapier migrate` and `zapier deprecate` before this command will work.",
|
|
1748
|
-
"flags": {
|
|
1749
|
-
"debug": {
|
|
1750
|
-
"char": "d",
|
|
1751
|
-
"description": "Show extra debugging output.",
|
|
1752
|
-
"name": "debug",
|
|
1753
|
-
"allowNo": false,
|
|
1754
|
-
"type": "boolean"
|
|
1755
|
-
},
|
|
1756
|
-
"invokedFromAnotherCommand": {
|
|
1757
|
-
"hidden": true,
|
|
1758
|
-
"name": "invokedFromAnotherCommand",
|
|
1759
|
-
"allowNo": false,
|
|
1760
|
-
"type": "boolean"
|
|
1761
|
-
}
|
|
1762
|
-
},
|
|
1763
|
-
"hasDynamicHelp": false,
|
|
1764
|
-
"hiddenAliases": [],
|
|
1765
|
-
"id": "delete:version",
|
|
1766
|
-
"pluginAlias": "zapier-platform-cli",
|
|
1767
|
-
"pluginName": "zapier-platform-cli",
|
|
1768
|
-
"pluginType": "core",
|
|
1769
|
-
"strict": true,
|
|
1770
|
-
"enableJsonFlag": false,
|
|
1771
|
-
"skipValidInstallCheck": true,
|
|
1772
|
-
"isESM": false,
|
|
1773
|
-
"relativePath": [
|
|
1774
|
-
"src",
|
|
1775
|
-
"oclif",
|
|
1776
|
-
"commands",
|
|
1777
|
-
"delete",
|
|
1778
|
-
"version.js"
|
|
1779
|
-
]
|
|
1780
|
-
},
|
|
1781
1707
|
"env:get": {
|
|
1782
1708
|
"aliases": [],
|
|
1783
1709
|
"args": {
|
|
@@ -1954,6 +1880,88 @@
|
|
|
1954
1880
|
"unset.js"
|
|
1955
1881
|
]
|
|
1956
1882
|
},
|
|
1883
|
+
"delete:integration": {
|
|
1884
|
+
"aliases": [
|
|
1885
|
+
"delete:app"
|
|
1886
|
+
],
|
|
1887
|
+
"args": {},
|
|
1888
|
+
"description": "Delete your integration (including all versions).\n\nThis only works if there are no active users or Zaps on any version. If you only want to delete certain versions, use the `zapier delete:version` command instead. It's unlikely that you'll be able to run this on an app that you've pushed publicly, since there are usually still users.",
|
|
1889
|
+
"flags": {
|
|
1890
|
+
"debug": {
|
|
1891
|
+
"char": "d",
|
|
1892
|
+
"description": "Show extra debugging output.",
|
|
1893
|
+
"name": "debug",
|
|
1894
|
+
"allowNo": false,
|
|
1895
|
+
"type": "boolean"
|
|
1896
|
+
},
|
|
1897
|
+
"invokedFromAnotherCommand": {
|
|
1898
|
+
"hidden": true,
|
|
1899
|
+
"name": "invokedFromAnotherCommand",
|
|
1900
|
+
"allowNo": false,
|
|
1901
|
+
"type": "boolean"
|
|
1902
|
+
}
|
|
1903
|
+
},
|
|
1904
|
+
"hasDynamicHelp": false,
|
|
1905
|
+
"hiddenAliases": [],
|
|
1906
|
+
"id": "delete:integration",
|
|
1907
|
+
"pluginAlias": "zapier-platform-cli",
|
|
1908
|
+
"pluginName": "zapier-platform-cli",
|
|
1909
|
+
"pluginType": "core",
|
|
1910
|
+
"strict": true,
|
|
1911
|
+
"enableJsonFlag": false,
|
|
1912
|
+
"skipValidInstallCheck": true,
|
|
1913
|
+
"isESM": false,
|
|
1914
|
+
"relativePath": [
|
|
1915
|
+
"src",
|
|
1916
|
+
"oclif",
|
|
1917
|
+
"commands",
|
|
1918
|
+
"delete",
|
|
1919
|
+
"integration.js"
|
|
1920
|
+
]
|
|
1921
|
+
},
|
|
1922
|
+
"delete:version": {
|
|
1923
|
+
"aliases": [],
|
|
1924
|
+
"args": {
|
|
1925
|
+
"version": {
|
|
1926
|
+
"description": "Specify the version to delete. It must have no users or Zaps.",
|
|
1927
|
+
"name": "version",
|
|
1928
|
+
"required": true
|
|
1929
|
+
}
|
|
1930
|
+
},
|
|
1931
|
+
"description": "Delete a specific version of your integration.\n\nThis only works if there are no users or Zaps on that version. You will probably need to have run `zapier migrate` and `zapier deprecate` before this command will work.",
|
|
1932
|
+
"flags": {
|
|
1933
|
+
"debug": {
|
|
1934
|
+
"char": "d",
|
|
1935
|
+
"description": "Show extra debugging output.",
|
|
1936
|
+
"name": "debug",
|
|
1937
|
+
"allowNo": false,
|
|
1938
|
+
"type": "boolean"
|
|
1939
|
+
},
|
|
1940
|
+
"invokedFromAnotherCommand": {
|
|
1941
|
+
"hidden": true,
|
|
1942
|
+
"name": "invokedFromAnotherCommand",
|
|
1943
|
+
"allowNo": false,
|
|
1944
|
+
"type": "boolean"
|
|
1945
|
+
}
|
|
1946
|
+
},
|
|
1947
|
+
"hasDynamicHelp": false,
|
|
1948
|
+
"hiddenAliases": [],
|
|
1949
|
+
"id": "delete:version",
|
|
1950
|
+
"pluginAlias": "zapier-platform-cli",
|
|
1951
|
+
"pluginName": "zapier-platform-cli",
|
|
1952
|
+
"pluginType": "core",
|
|
1953
|
+
"strict": true,
|
|
1954
|
+
"enableJsonFlag": false,
|
|
1955
|
+
"skipValidInstallCheck": true,
|
|
1956
|
+
"isESM": false,
|
|
1957
|
+
"relativePath": [
|
|
1958
|
+
"src",
|
|
1959
|
+
"oclif",
|
|
1960
|
+
"commands",
|
|
1961
|
+
"delete",
|
|
1962
|
+
"version.js"
|
|
1963
|
+
]
|
|
1964
|
+
},
|
|
1957
1965
|
"team:add": {
|
|
1958
1966
|
"aliases": [
|
|
1959
1967
|
"team:invite"
|
|
@@ -2333,5 +2341,5 @@
|
|
|
2333
2341
|
]
|
|
2334
2342
|
}
|
|
2335
2343
|
},
|
|
2336
|
-
"version": "16.
|
|
2344
|
+
"version": "16.3.1"
|
|
2337
2345
|
}
|
package/package.json
CHANGED
package/src/generators/index.js
CHANGED
|
@@ -201,6 +201,7 @@ const TEMPLATE_ROUTES = {
|
|
|
201
201
|
minimal: writeForMinimalTemplate,
|
|
202
202
|
'oauth1-trello': writeForAuthTemplate,
|
|
203
203
|
oauth2: writeForAuthTemplate,
|
|
204
|
+
openai: writeForStandaloneTemplate,
|
|
204
205
|
'search-or-create': writeForStandaloneTemplate,
|
|
205
206
|
'session-auth': writeForAuthTemplate,
|
|
206
207
|
typescript: writeForStandaloneTypeScriptTemplate,
|
package/src/generators/pull.js
CHANGED
|
@@ -24,7 +24,10 @@ module.exports = class PullGenerator extends Generator {
|
|
|
24
24
|
name: 'confirm',
|
|
25
25
|
message: `Warning: You are about to overwrite existing files.
|
|
26
26
|
|
|
27
|
-
Before proceeding, please make sure you have saved your work. Consider creating a backup or saving your current state in a git branch.
|
|
27
|
+
Before proceeding, please make sure you have saved your work. Consider creating a backup or saving your current state in a git branch.
|
|
28
|
+
|
|
29
|
+
If presented with a series of options ('ynarxdeiH'), you may
|
|
30
|
+
press Enter to view more details about each option. For example, 'x' will abort the process.
|
|
28
31
|
|
|
29
32
|
Do you want to continue?`,
|
|
30
33
|
default: false,
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
# OpenAI
|
|
2
|
+
|
|
3
|
+
This Zapier integration project is generated by the `zapier init` CLI command. This integration in particular is using the OpenAI API to generate responses to prompts from users. For this integration, there is a `constants.js` file that will allow you to swap out the base URL and version of the API to swap if you are using an OpenAI compatible API to get started.
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
const { API_URL } = require('./constants');
|
|
2
|
+
// You want to make a request to an endpoint that is either specifically designed
|
|
3
|
+
// to test auth, or one that every user will have access to. eg: `/me`.
|
|
4
|
+
// By returning the entire request object, you have access to the request and
|
|
5
|
+
// response data for testing purposes. Your connection label can access any data
|
|
6
|
+
// from the returned response using the `json.` prefix. eg: `{{json.username}}`.
|
|
7
|
+
const test = (z, bundle) => z.request({ url: `${API_URL}/me` });
|
|
8
|
+
|
|
9
|
+
module.exports = {
|
|
10
|
+
// "custom" is the catch-all auth type. The user supplies some info and Zapier can
|
|
11
|
+
// make authenticated requests with it
|
|
12
|
+
type: 'custom',
|
|
13
|
+
|
|
14
|
+
// Define any input app's auth requires here. The user will be prompted to enter
|
|
15
|
+
// this info when they connect their account.
|
|
16
|
+
fields: [
|
|
17
|
+
{
|
|
18
|
+
key: 'api_key',
|
|
19
|
+
label: 'API Key',
|
|
20
|
+
required: true,
|
|
21
|
+
helpText:
|
|
22
|
+
'Generate an API Key in your [Platform settings page](https://platform.openai.com/api-keys).',
|
|
23
|
+
},
|
|
24
|
+
// This field is optional and can be removed if not needed
|
|
25
|
+
{
|
|
26
|
+
key: 'organization_id',
|
|
27
|
+
required: false,
|
|
28
|
+
label: 'Organization ID',
|
|
29
|
+
helpText:
|
|
30
|
+
'**Optional** Only required if your OpenAI account belongs to multiple organizations. If not using OpenAI, this field will be disregarded. If your OpenAI account belongs to multiple organizations, optionally add the [Organization ID](https://platform.openai.com/account/org-settings) that this connection should use. If left blank, your [default organization](https://platform.openai.com/account/api-keys) will be used.',
|
|
31
|
+
},
|
|
32
|
+
],
|
|
33
|
+
|
|
34
|
+
// The test method allows Zapier to verify that the credentials a user provides
|
|
35
|
+
// are valid. We'll execute this method whenever a user connects their account for
|
|
36
|
+
// the first time.
|
|
37
|
+
test,
|
|
38
|
+
|
|
39
|
+
// This template string can access all the data returned from the auth test. If
|
|
40
|
+
// you return the test object, you'll access the returned data with a label like
|
|
41
|
+
// `{{json.X}}`. If you return `response.data` from your test, then your label can
|
|
42
|
+
// be `{{X}}`. This can also be a function that returns a label. That function has
|
|
43
|
+
// the standard args `(z, bundle)` and data returned from the test can be accessed
|
|
44
|
+
// in `bundle.inputData.X`.
|
|
45
|
+
connectionLabel: '{{json.email}}',
|
|
46
|
+
};
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/* eslint-disable camelcase */
|
|
2
|
+
const { API_URL, DEFAULT_MODEL } = require('../constants');
|
|
3
|
+
|
|
4
|
+
const sample = require('../samples/chat.json');
|
|
5
|
+
|
|
6
|
+
async function getAdvancedFields(_z, bundle) {
|
|
7
|
+
if (bundle.inputData.show_advanced === true) {
|
|
8
|
+
return [
|
|
9
|
+
{
|
|
10
|
+
key: 'info_advanced',
|
|
11
|
+
type: 'copy',
|
|
12
|
+
helpText:
|
|
13
|
+
"The following fields are for advanced users and should be used with caution as they may affect performance. In most cases, the default options are sufficient. If you'd like to explore these options further, you can [learn more here](https://help.zapier.com/hc/en-us/articles/22497191078797).",
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
key: 'developer_message',
|
|
17
|
+
label: 'Developer/System Message',
|
|
18
|
+
type: 'text',
|
|
19
|
+
helpText:
|
|
20
|
+
'Instructions to the model that are prioritized ahead of user messages, following [chain of command](https://cdn.openai.com/spec/model-spec-2024-05-08.html#follow-the-chain-of-command).',
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
key: 'temperature',
|
|
24
|
+
label: 'Temperature',
|
|
25
|
+
type: 'number',
|
|
26
|
+
helpText:
|
|
27
|
+
'Higher values mean the model will take more risks. Try 0.9 for more creative applications, and 0 for ones with a well-defined answer.\n\nUse a decimal between 0 and 1.',
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
key: 'max_completion_tokens',
|
|
31
|
+
label: 'Maximum Length',
|
|
32
|
+
type: 'integer',
|
|
33
|
+
helpText: 'The maximum number of tokens for the completion.',
|
|
34
|
+
},
|
|
35
|
+
];
|
|
36
|
+
}
|
|
37
|
+
return [];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async function perform(z, bundle) {
|
|
41
|
+
const {
|
|
42
|
+
user_message,
|
|
43
|
+
model,
|
|
44
|
+
files,
|
|
45
|
+
developer_message,
|
|
46
|
+
temperature,
|
|
47
|
+
max_completion_tokens,
|
|
48
|
+
} = bundle.inputData;
|
|
49
|
+
|
|
50
|
+
const developerMessage = {
|
|
51
|
+
role: 'developer',
|
|
52
|
+
content: [
|
|
53
|
+
{
|
|
54
|
+
type: 'text',
|
|
55
|
+
text: developer_message || 'You are a helpful assistant.',
|
|
56
|
+
},
|
|
57
|
+
],
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const userMessage = {
|
|
61
|
+
role: 'user',
|
|
62
|
+
content: [
|
|
63
|
+
{
|
|
64
|
+
type: 'text',
|
|
65
|
+
text: user_message,
|
|
66
|
+
},
|
|
67
|
+
...(files
|
|
68
|
+
? files.map((file) => ({
|
|
69
|
+
type: 'image_url',
|
|
70
|
+
image_url: {
|
|
71
|
+
url: file,
|
|
72
|
+
},
|
|
73
|
+
}))
|
|
74
|
+
: []),
|
|
75
|
+
],
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const messages = [developerMessage, userMessage];
|
|
79
|
+
|
|
80
|
+
const response = await z.request({
|
|
81
|
+
url: `${API_URL}/chat/completions`,
|
|
82
|
+
method: 'POST',
|
|
83
|
+
body: JSON.stringify({
|
|
84
|
+
model,
|
|
85
|
+
messages,
|
|
86
|
+
temperature,
|
|
87
|
+
max_completion_tokens,
|
|
88
|
+
}),
|
|
89
|
+
});
|
|
90
|
+
return response.data;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
module.exports = {
|
|
94
|
+
key: 'chat_completion',
|
|
95
|
+
noun: 'Chat',
|
|
96
|
+
display: {
|
|
97
|
+
label: 'Chat Completion',
|
|
98
|
+
description: 'Sends a Chat to OpenAI and generates a Completion.',
|
|
99
|
+
},
|
|
100
|
+
operation: {
|
|
101
|
+
perform,
|
|
102
|
+
inputFields: [
|
|
103
|
+
{
|
|
104
|
+
key: 'info_data_usage',
|
|
105
|
+
type: 'copy',
|
|
106
|
+
helpText:
|
|
107
|
+
"Data sent to OpenAI through this Zap is via an API. Under OpenAI's [API data usage policy](https://openai.com/policies/api-data-usage-policies), OpenAI will not use API-submitted data to train or improve their models unless you explicitly decide to share your data with them for that purpose (such as by opting in). For more information, please review OpenAI's article about [when/how data may be used to improve model performance](https://help.openai.com/en/articles/5722486-how-your-data-is-used-to-improve-model-performance).",
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
key: 'user_message',
|
|
111
|
+
label: 'User Message',
|
|
112
|
+
type: 'text',
|
|
113
|
+
helpText:
|
|
114
|
+
"Instructions that request some output from the model. Similar to messages you'd type in [ChatGPT](https://chatgpt.com) as an end user.",
|
|
115
|
+
required: true,
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
key: 'files',
|
|
119
|
+
label: 'Images',
|
|
120
|
+
type: 'file',
|
|
121
|
+
helpText: 'Images to include along with your message.',
|
|
122
|
+
list: true,
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
key: 'model',
|
|
126
|
+
label: 'Model',
|
|
127
|
+
type: 'string',
|
|
128
|
+
required: true,
|
|
129
|
+
default: DEFAULT_MODEL, // Optional to default to a specific model for most users
|
|
130
|
+
dynamic: 'list_models.id.name',
|
|
131
|
+
altersDynamicFields: false,
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
key: 'show_advanced',
|
|
135
|
+
label: 'Show Advanced Options',
|
|
136
|
+
type: 'boolean',
|
|
137
|
+
default: 'false',
|
|
138
|
+
altersDynamicFields: true,
|
|
139
|
+
},
|
|
140
|
+
getAdvancedFields,
|
|
141
|
+
],
|
|
142
|
+
// Rename some of the output fields to be more descriptive for a user
|
|
143
|
+
outputFields: [
|
|
144
|
+
{ key: 'id', type: 'string', label: 'Completion ID' },
|
|
145
|
+
{ key: 'model', type: 'string', label: 'Model' },
|
|
146
|
+
{
|
|
147
|
+
key: 'usage__prompt_tokens',
|
|
148
|
+
type: 'number',
|
|
149
|
+
label: 'Usage: Prompt Tokens',
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
key: 'usage__completion_tokens',
|
|
153
|
+
type: 'number',
|
|
154
|
+
label: 'Usage: Completion Tokens',
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
key: 'usage__total_tokens',
|
|
158
|
+
type: 'number',
|
|
159
|
+
label: 'Usage: Total Tokens',
|
|
160
|
+
},
|
|
161
|
+
],
|
|
162
|
+
sample,
|
|
163
|
+
},
|
|
164
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
const { API_URL } = require('../constants');
|
|
2
|
+
|
|
3
|
+
const perform = async (z, bundle) => {
|
|
4
|
+
const response = await z.request({ url: `${API_URL}/models` });
|
|
5
|
+
|
|
6
|
+
const responseData = response.data;
|
|
7
|
+
|
|
8
|
+
return responseData.data.map((model) => ({
|
|
9
|
+
id: model.id,
|
|
10
|
+
name: model.id,
|
|
11
|
+
}));
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
module.exports = {
|
|
15
|
+
key: 'list_models',
|
|
16
|
+
noun: 'Model',
|
|
17
|
+
display: {
|
|
18
|
+
label: 'List of Models',
|
|
19
|
+
description:
|
|
20
|
+
'This is a hidden trigger, and is used in a Dynamic Dropdown of another trigger.',
|
|
21
|
+
hidden: true,
|
|
22
|
+
},
|
|
23
|
+
operation: { perform },
|
|
24
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/* eslint-disable camelcase */
|
|
2
|
+
const authentication = require('./authentication');
|
|
3
|
+
const middleware = require('./middleware');
|
|
4
|
+
const dynamic_dropdowns = require('./dynamic_dropdowns');
|
|
5
|
+
const creates = require('./creates');
|
|
6
|
+
|
|
7
|
+
module.exports = {
|
|
8
|
+
// This is just shorthand to reference the installed dependencies you have.
|
|
9
|
+
// Zapier will need to know these before we can upload.
|
|
10
|
+
version: require('./package.json').version,
|
|
11
|
+
platformVersion: require('zapier-platform-core').version,
|
|
12
|
+
|
|
13
|
+
authentication,
|
|
14
|
+
|
|
15
|
+
beforeRequest: [...middleware.befores],
|
|
16
|
+
|
|
17
|
+
afterResponse: [...middleware.afters],
|
|
18
|
+
|
|
19
|
+
// If you want your trigger to show up, you better include it here!
|
|
20
|
+
triggers: {
|
|
21
|
+
...dynamic_dropdowns,
|
|
22
|
+
},
|
|
23
|
+
|
|
24
|
+
// If you want your searches to show up, you better include it here!
|
|
25
|
+
searches: {},
|
|
26
|
+
|
|
27
|
+
// If you want your creates to show up, you better include it here!
|
|
28
|
+
creates: {
|
|
29
|
+
...creates,
|
|
30
|
+
},
|
|
31
|
+
|
|
32
|
+
resources: {},
|
|
33
|
+
};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/* eslint-disable camelcase */
|
|
2
|
+
// This function runs after every outbound request. You can use it to check for
|
|
3
|
+
// errors or modify the response. You can have as many as you need. They'll need
|
|
4
|
+
// to each be registered in your index.js file.
|
|
5
|
+
const handleBadResponses = (response, z, bundle) => {
|
|
6
|
+
if (response.data.error) {
|
|
7
|
+
throw new z.errors.Error(
|
|
8
|
+
response.data.error.message,
|
|
9
|
+
response.data.error.code,
|
|
10
|
+
response.status,
|
|
11
|
+
);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
return response;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const includeOrgId = (request, z, bundle) => {
|
|
18
|
+
const { organization_id } = bundle.authData;
|
|
19
|
+
if (organization_id) {
|
|
20
|
+
request.headers['OpenAI-Organization'] = organization_id;
|
|
21
|
+
}
|
|
22
|
+
return request;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
// This function runs before every outbound request. You can have as many as you
|
|
26
|
+
// need. They'll need to each be registered in your index.js file.
|
|
27
|
+
const includeApiKey = (request, z, bundle) => {
|
|
28
|
+
const { api_key } = bundle.authData;
|
|
29
|
+
if (api_key) {
|
|
30
|
+
// Use these lines to include the API key in the querystring
|
|
31
|
+
// request.params = request.params || {};
|
|
32
|
+
// request.params.api_key = api_key;
|
|
33
|
+
|
|
34
|
+
// If you want to include the API key in the header:
|
|
35
|
+
request.headers.Authorization = `Bearer ${api_key}`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return request;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const jsonHeaders = (request) => {
|
|
42
|
+
request.headers['Content-Type'] = 'application/json';
|
|
43
|
+
request.headers.Accept = 'application/json';
|
|
44
|
+
return request;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
module.exports = {
|
|
48
|
+
befores: [includeApiKey, includeOrgId, jsonHeaders],
|
|
49
|
+
afters: [handleBadResponses],
|
|
50
|
+
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "chatcmpl-123",
|
|
3
|
+
"object": "chat.completion",
|
|
4
|
+
"created": 1677652288,
|
|
5
|
+
"model": "gpt-4o-mini",
|
|
6
|
+
"system_fingerprint": "fp_44709d6fcb",
|
|
7
|
+
"choices": [
|
|
8
|
+
{
|
|
9
|
+
"index": 0,
|
|
10
|
+
"message": {
|
|
11
|
+
"role": "assistant",
|
|
12
|
+
"content": "\n\nHello there, how may I assist you today?"
|
|
13
|
+
},
|
|
14
|
+
"logprobs": null,
|
|
15
|
+
"finish_reason": "stop"
|
|
16
|
+
}
|
|
17
|
+
],
|
|
18
|
+
"service_tier": "default",
|
|
19
|
+
"usage": {
|
|
20
|
+
"prompt_tokens": 9,
|
|
21
|
+
"completion_tokens": 12,
|
|
22
|
+
"total_tokens": 21,
|
|
23
|
+
"completion_tokens_details": {
|
|
24
|
+
"reasoning_tokens": 0,
|
|
25
|
+
"accepted_prediction_tokens": 0,
|
|
26
|
+
"rejected_prediction_tokens": 0
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/* globals describe, it, expect */
|
|
2
|
+
/* eslint-disable no-undef */
|
|
3
|
+
|
|
4
|
+
const App = require('../index');
|
|
5
|
+
|
|
6
|
+
describe('custom auth', () => {
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
jest.clearAllMocks();
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it('passes authentication and returns json', async () => {
|
|
12
|
+
const bundle = {
|
|
13
|
+
authData: {
|
|
14
|
+
api_key: 'secret',
|
|
15
|
+
},
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
// Mock successful response
|
|
19
|
+
const mockResponse = {
|
|
20
|
+
status: 200,
|
|
21
|
+
data: {
|
|
22
|
+
email: 'test@example.com',
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
// Mock the request client
|
|
27
|
+
const mockRequest = jest.fn().mockResolvedValue(mockResponse);
|
|
28
|
+
const z = {
|
|
29
|
+
request: mockRequest,
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const response = await App.authentication.test(z, bundle);
|
|
33
|
+
|
|
34
|
+
expect(mockRequest).toHaveBeenCalledTimes(1);
|
|
35
|
+
expect(mockRequest).toHaveBeenCalledWith({
|
|
36
|
+
url: expect.stringContaining('/me'),
|
|
37
|
+
});
|
|
38
|
+
expect(response.status).toBe(200);
|
|
39
|
+
expect(response.data).toHaveProperty('email', 'test@example.com');
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('fails on bad auth', async () => {
|
|
43
|
+
const bundle = {
|
|
44
|
+
authData: {
|
|
45
|
+
api_key: 'bad',
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
// Mock failed response
|
|
50
|
+
const mockRequest = jest
|
|
51
|
+
.fn()
|
|
52
|
+
.mockRejectedValue(new Error('Incorrect API key provided'));
|
|
53
|
+
const z = {
|
|
54
|
+
request: mockRequest,
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
await App.authentication.test(z, bundle);
|
|
59
|
+
} catch (error) {
|
|
60
|
+
expect(mockRequest).toHaveBeenCalledTimes(1);
|
|
61
|
+
expect(error.message).toContain('Incorrect API key provided');
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
throw new Error('appTester should have thrown');
|
|
65
|
+
});
|
|
66
|
+
});
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/* globals describe, it, expect */
|
|
2
|
+
/* eslint-disable no-undef */
|
|
3
|
+
|
|
4
|
+
const chatCompletion = require('../creates/chat_completion');
|
|
5
|
+
const { DEFAULT_MODEL } = require('../constants');
|
|
6
|
+
|
|
7
|
+
describe('chat_completion', () => {
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
jest.clearAllMocks();
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it('creates a basic chat completion', async () => {
|
|
13
|
+
const bundle = {
|
|
14
|
+
inputData: {
|
|
15
|
+
user_message: 'Hello, how are you?',
|
|
16
|
+
model: DEFAULT_MODEL,
|
|
17
|
+
},
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const mockResponse = {
|
|
21
|
+
data: {
|
|
22
|
+
id: 'chatcmpl-123',
|
|
23
|
+
model: DEFAULT_MODEL,
|
|
24
|
+
usage: {
|
|
25
|
+
prompt_tokens: 20,
|
|
26
|
+
completion_tokens: 15,
|
|
27
|
+
total_tokens: 35,
|
|
28
|
+
},
|
|
29
|
+
choices: [
|
|
30
|
+
{
|
|
31
|
+
message: {
|
|
32
|
+
content: 'I am doing well, thank you for asking!',
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
],
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const mockRequest = jest.fn().mockResolvedValue(mockResponse);
|
|
40
|
+
const z = { request: mockRequest };
|
|
41
|
+
|
|
42
|
+
const result = await chatCompletion.operation.perform(z, bundle);
|
|
43
|
+
|
|
44
|
+
expect(mockRequest).toHaveBeenCalledTimes(1);
|
|
45
|
+
expect(mockRequest).toHaveBeenCalledWith({
|
|
46
|
+
url: expect.stringContaining('/chat/completions'),
|
|
47
|
+
method: 'POST',
|
|
48
|
+
body: expect.stringContaining(bundle.inputData.user_message),
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
expect(result).toEqual(mockResponse.data);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('creates a chat completion with advanced options', async () => {
|
|
55
|
+
const bundle = {
|
|
56
|
+
inputData: {
|
|
57
|
+
user_message: 'Write a story',
|
|
58
|
+
model: DEFAULT_MODEL,
|
|
59
|
+
developer_message: 'You are a creative writer',
|
|
60
|
+
temperature: 0.9,
|
|
61
|
+
max_completion_tokens: 100,
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const mockResponse = {
|
|
66
|
+
data: {
|
|
67
|
+
id: 'chatcmpl-456',
|
|
68
|
+
model: DEFAULT_MODEL,
|
|
69
|
+
usage: {
|
|
70
|
+
prompt_tokens: 25,
|
|
71
|
+
completion_tokens: 50,
|
|
72
|
+
total_tokens: 75,
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const mockRequest = jest.fn().mockResolvedValue(mockResponse);
|
|
78
|
+
const z = { request: mockRequest };
|
|
79
|
+
|
|
80
|
+
const result = await chatCompletion.operation.perform(z, bundle);
|
|
81
|
+
|
|
82
|
+
expect(mockRequest).toHaveBeenCalledTimes(1);
|
|
83
|
+
expect(mockRequest).toHaveBeenCalledWith({
|
|
84
|
+
url: expect.stringContaining('/chat/completions'),
|
|
85
|
+
method: 'POST',
|
|
86
|
+
body: expect.stringMatching(/temperature.*0.9/),
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
expect(result).toEqual(mockResponse.data);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('creates a chat completion with image', async () => {
|
|
93
|
+
const bundle = {
|
|
94
|
+
inputData: {
|
|
95
|
+
user_message: 'Describe this image',
|
|
96
|
+
model: DEFAULT_MODEL,
|
|
97
|
+
files: ['https://example.com/image.jpg'],
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const mockResponse = {
|
|
102
|
+
data: {
|
|
103
|
+
id: 'chatcmpl-789',
|
|
104
|
+
model: DEFAULT_MODEL,
|
|
105
|
+
usage: {
|
|
106
|
+
prompt_tokens: 30,
|
|
107
|
+
completion_tokens: 20,
|
|
108
|
+
total_tokens: 50,
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const mockRequest = jest.fn().mockResolvedValue(mockResponse);
|
|
114
|
+
const z = { request: mockRequest };
|
|
115
|
+
|
|
116
|
+
const result = await chatCompletion.operation.perform(z, bundle);
|
|
117
|
+
|
|
118
|
+
expect(mockRequest).toHaveBeenCalledTimes(1);
|
|
119
|
+
expect(mockRequest).toHaveBeenCalledWith({
|
|
120
|
+
url: expect.stringContaining('/chat/completions'),
|
|
121
|
+
method: 'POST',
|
|
122
|
+
body: expect.stringMatching(/image_url.*example.com/),
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
expect(result).toEqual(mockResponse.data);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('handles API errors', async () => {
|
|
129
|
+
const bundle = {
|
|
130
|
+
inputData: {
|
|
131
|
+
user_message: 'Hello',
|
|
132
|
+
model: DEFAULT_MODEL,
|
|
133
|
+
},
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
const mockRequest = jest
|
|
137
|
+
.fn()
|
|
138
|
+
.mockRejectedValue(new Error('Invalid request'));
|
|
139
|
+
const z = { request: mockRequest };
|
|
140
|
+
|
|
141
|
+
try {
|
|
142
|
+
await chatCompletion.operation.perform(z, bundle);
|
|
143
|
+
} catch (error) {
|
|
144
|
+
expect(mockRequest).toHaveBeenCalledTimes(1);
|
|
145
|
+
expect(error.message).toContain('Invalid request');
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
throw new Error('Should have thrown an error');
|
|
149
|
+
});
|
|
150
|
+
});
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/* globals describe, it, expect */
|
|
2
|
+
/* eslint-disable no-undef */
|
|
3
|
+
|
|
4
|
+
const listModels = require('../dynamic_dropdowns/list_models');
|
|
5
|
+
|
|
6
|
+
describe('list_models', () => {
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
jest.clearAllMocks();
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it('returns formatted list of models', async () => {
|
|
12
|
+
const mockResponse = {
|
|
13
|
+
data: {
|
|
14
|
+
data: [{ id: 'gpt-4' }, { id: 'gpt-3.5-turbo' }],
|
|
15
|
+
},
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const mockRequest = jest.fn().mockResolvedValue(mockResponse);
|
|
19
|
+
const z = { request: mockRequest };
|
|
20
|
+
const bundle = {};
|
|
21
|
+
|
|
22
|
+
const results = await listModels.operation.perform(z, bundle);
|
|
23
|
+
|
|
24
|
+
expect(mockRequest).toHaveBeenCalledTimes(1);
|
|
25
|
+
expect(mockRequest).toHaveBeenCalledWith({
|
|
26
|
+
url: expect.stringContaining('/models'),
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
expect(results).toEqual([
|
|
30
|
+
{ id: 'gpt-4', name: 'gpt-4' },
|
|
31
|
+
{ id: 'gpt-3.5-turbo', name: 'gpt-3.5-turbo' },
|
|
32
|
+
]);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('handles API errors', async () => {
|
|
36
|
+
const mockRequest = jest
|
|
37
|
+
.fn()
|
|
38
|
+
.mockRejectedValue(new Error('Failed to fetch models'));
|
|
39
|
+
const z = { request: mockRequest };
|
|
40
|
+
const bundle = {};
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
await listModels.operation.perform(z, bundle);
|
|
44
|
+
} catch (error) {
|
|
45
|
+
expect(mockRequest).toHaveBeenCalledTimes(1);
|
|
46
|
+
expect(error.message).toContain('Failed to fetch models');
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
throw new Error('Should have thrown an error');
|
|
50
|
+
});
|
|
51
|
+
});
|
|
@@ -259,7 +259,7 @@ const getAuthLabel = async (labelTemplate, authData, meta, zcacheTestObj) => {
|
|
|
259
259
|
const testResult = await testAuth(authData, meta, zcacheTestObj);
|
|
260
260
|
labelTemplate = labelTemplate.replace('__', '.');
|
|
261
261
|
const tpl = _.template(labelTemplate, { interpolate: /{{([\s\S]+?)}}/g });
|
|
262
|
-
return tpl(testResult);
|
|
262
|
+
return tpl({ ...testResult, bundle: { authData, inputData: testResult } });
|
|
263
263
|
};
|
|
264
264
|
|
|
265
265
|
const getLabelForDynamicDropdown = (obj, preferredKey, fallbackKey) => {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const ZapierBaseCommand = require('../ZapierBaseCommand');
|
|
2
2
|
const { BUILD_PATH, SOURCE_PATH } = require('../../constants');
|
|
3
|
+
const { Flags } = require('@oclif/core');
|
|
3
4
|
|
|
4
5
|
const BuildCommand = require('./build');
|
|
5
6
|
|
|
@@ -13,6 +14,7 @@ class PushCommand extends ZapierBaseCommand {
|
|
|
13
14
|
skipNpmInstall: this.flags['skip-npm-install'],
|
|
14
15
|
disableDependencyDetection: this.flags['disable-dependency-detection'],
|
|
15
16
|
skipValidation: this.flags['skip-validation'],
|
|
17
|
+
overwritePartnerChanges: this.flags['overwrite-partner-changes'],
|
|
16
18
|
},
|
|
17
19
|
);
|
|
18
20
|
this.log(
|
|
@@ -21,7 +23,14 @@ class PushCommand extends ZapierBaseCommand {
|
|
|
21
23
|
}
|
|
22
24
|
}
|
|
23
25
|
|
|
24
|
-
PushCommand.flags =
|
|
26
|
+
PushCommand.flags = {
|
|
27
|
+
...BuildCommand.flags,
|
|
28
|
+
'overwrite-partner-changes': Flags.boolean({
|
|
29
|
+
description:
|
|
30
|
+
'(Internal Use Only) Allows Zapier Staff to push changes to integrations in certain situations.',
|
|
31
|
+
hidden: true,
|
|
32
|
+
}),
|
|
33
|
+
};
|
|
25
34
|
PushCommand.description = `Build and upload the current integration.
|
|
26
35
|
|
|
27
36
|
This command is the same as running \`zapier build\` and \`zapier upload\` in sequence. See those for more info.`;
|
package/src/utils/api.js
CHANGED
|
@@ -422,7 +422,10 @@ const downloadSourceZip = async (dst) => {
|
|
|
422
422
|
}
|
|
423
423
|
};
|
|
424
424
|
|
|
425
|
-
const upload = async (
|
|
425
|
+
const upload = async (
|
|
426
|
+
app,
|
|
427
|
+
{ skipValidation = false, overwritePartnerChanges = false } = {},
|
|
428
|
+
) => {
|
|
426
429
|
const zipPath = constants.BUILD_PATH;
|
|
427
430
|
const sourceZipPath = constants.SOURCE_PATH;
|
|
428
431
|
const appDir = process.cwd();
|
|
@@ -449,17 +452,42 @@ const upload = async (app, { skipValidation = false } = {}) => {
|
|
|
449
452
|
const binarySourceZip = fs.readFileSync(fullSourceZipPath);
|
|
450
453
|
const sourceBuffer = Buffer.from(binarySourceZip).toString('base64');
|
|
451
454
|
|
|
455
|
+
const headers = {};
|
|
456
|
+
if (overwritePartnerChanges) {
|
|
457
|
+
headers['X-Overwrite-Partner-Changes'] = 'true';
|
|
458
|
+
}
|
|
459
|
+
|
|
452
460
|
startSpinner(`Uploading version ${definition.version}`);
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
+
try {
|
|
462
|
+
await callAPI(
|
|
463
|
+
`/apps/${app.id}/versions/${definition.version}`,
|
|
464
|
+
{
|
|
465
|
+
method: 'PUT',
|
|
466
|
+
body: {
|
|
467
|
+
zip_file: buffer,
|
|
468
|
+
source_zip_file: sourceBuffer,
|
|
469
|
+
skip_validation: skipValidation,
|
|
470
|
+
},
|
|
471
|
+
extraHeaders: headers,
|
|
472
|
+
},
|
|
473
|
+
true,
|
|
474
|
+
);
|
|
475
|
+
} catch (err) {
|
|
476
|
+
endSpinner({ success: false });
|
|
477
|
+
// 409 from the backend specifically signals that the last changes were from a partner
|
|
478
|
+
// and this is a staff user which could unintentionally overwrite those changes
|
|
479
|
+
if (err.status === 409) {
|
|
480
|
+
throw new Error(
|
|
481
|
+
`The latest integration changes appear to be from a partner. OK to overwrite?` +
|
|
482
|
+
` If so, run this command again using the '--overwrite-partner-changes' flag.`,
|
|
483
|
+
);
|
|
484
|
+
}
|
|
461
485
|
|
|
462
|
-
|
|
486
|
+
// Don't ignore other errors, re-throw them with a user-friendly message
|
|
487
|
+
throw new Error(err.errText);
|
|
488
|
+
} finally {
|
|
489
|
+
endSpinner();
|
|
490
|
+
}
|
|
463
491
|
};
|
|
464
492
|
|
|
465
493
|
module.exports = {
|