onetagai 0.0.0-stage → 1.2.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 +21 -0
- package/README.md +73 -2
- package/core.js +242 -0
- package/index.d.ts +78 -0
- package/index.js +53 -0
- package/package.json +30 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 OdysseyX LLC
|
|
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
CHANGED
|
@@ -1,3 +1,74 @@
|
|
|
1
|
-
#
|
|
1
|
+
# onetagai
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
AI for static sites without a backend. Call a model from the browser with a public key: no server, no secret in your code.
|
|
4
|
+
|
|
5
|
+
```sh
|
|
6
|
+
npm install onetagai
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
```js
|
|
10
|
+
import { ask } from "onetagai";
|
|
11
|
+
|
|
12
|
+
const summary = await ask("Summarize this page in two sentences", document.querySelector("article"));
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
It works on localhost right away, on a shared demo key. Before you deploy, get your own free key (email only, 1,000,000 tokens a month):
|
|
16
|
+
|
|
17
|
+
```sh
|
|
18
|
+
curl -X POST https://onetagai.com/api/signup -H "Content-Type: application/json" -d '{"email":"you@example.com"}'
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
```js
|
|
22
|
+
import { configure } from "onetagai";
|
|
23
|
+
|
|
24
|
+
configure({ key: "ot_..." });
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
The key is meant to be public, like an analytics ID: it goes in your page source.
|
|
28
|
+
|
|
29
|
+
## ask(prompt, input?, options?)
|
|
30
|
+
|
|
31
|
+
Returns a promise of the answer as text.
|
|
32
|
+
|
|
33
|
+
`input` is what the prompt is about: a string, a DOM element (its text), a form (its fields), an image or `File`, any JSON value, or an array of those.
|
|
34
|
+
|
|
35
|
+
| Option | What it does |
|
|
36
|
+
| --- | --- |
|
|
37
|
+
| `stream` | A function called with each piece of the answer as it arrives |
|
|
38
|
+
| `json` | `true` parses the answer and returns the value |
|
|
39
|
+
| `system` | Instructions for the model: a persona, limits, a format |
|
|
40
|
+
| `history` | Earlier turns, `[{ role: "user" \| "assistant", content }]` |
|
|
41
|
+
| `maxTokens` | Cap on the answer length (default 500) |
|
|
42
|
+
| `signal` | An `AbortSignal` to cancel the call |
|
|
43
|
+
|
|
44
|
+
```js
|
|
45
|
+
// Stream into the page
|
|
46
|
+
await ask("Explain this error", log, { stream: (delta) => (output.textContent += delta) });
|
|
47
|
+
|
|
48
|
+
// Structured output
|
|
49
|
+
const { tags } = await ask("Return {\"tags\": string[]} for this post", post, { json: true });
|
|
50
|
+
|
|
51
|
+
// An image
|
|
52
|
+
const alt = await ask("Write alt text for this image", fileInput.files[0]);
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
A failed call rejects with a `OneTagError`. Its `code` is one of `no_key`, `bad_key`, `rate_limited`, `quota_exceeded`, `too_large`, `network`, `aborted` and a few more; its `message` says what to do.
|
|
56
|
+
|
|
57
|
+
## Without a bundler
|
|
58
|
+
|
|
59
|
+
The same SDK as one script tag:
|
|
60
|
+
|
|
61
|
+
```html
|
|
62
|
+
<script src="https://onetagai.com/v1.js" data-key="ot_..."></script>
|
|
63
|
+
<script>
|
|
64
|
+
onetag.ask("Summarize this page", document.body).then(console.log);
|
|
65
|
+
</script>
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Links
|
|
69
|
+
|
|
70
|
+
- Docs: https://onetagai.com/docs
|
|
71
|
+
- For AI coding agents: https://onetagai.com/llms.txt
|
|
72
|
+
- Copy-paste blocks built on it: https://aiblock.onetagai.com
|
|
73
|
+
|
|
74
|
+
MIT
|
package/core.js
ADDED
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
/*! OneTag AI SDK v1.2.0 | https://onetagai.com | generated from sdk/v1.js by build.mjs, do not edit */
|
|
2
|
+
const MAX_TEXT = 32000;
|
|
3
|
+
const MAX_SIDE = 1600;
|
|
4
|
+
const CONTROLS = ["HTMLInputElement", "HTMLTextAreaElement", "HTMLSelectElement"];
|
|
5
|
+
const HOME = "https://onetagai.com";
|
|
6
|
+
const script = null;
|
|
7
|
+
const data = (script && script.dataset) || {};
|
|
8
|
+
// The API lives where the SDK came from when that is OneTag AI itself or a local dev
|
|
9
|
+
// server; loaded from a CDN (jsDelivr, unpkg) or a copy on your own site, it is HOME.
|
|
10
|
+
// data-endpoint on the script tag overrides either.
|
|
11
|
+
const origin = (() => {
|
|
12
|
+
try {
|
|
13
|
+
return script && script.src ? new URL(script.src) : null;
|
|
14
|
+
} catch (e) {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
})();
|
|
18
|
+
const OWN = origin && /^(onetagai\.com|htmlai\.000ooo\.ooo|localhost|127\.0\.0\.1)$/.test(origin.hostname);
|
|
19
|
+
const SITE = OWN ? origin.origin : HOME;
|
|
20
|
+
const config = {
|
|
21
|
+
key: data.key || "",
|
|
22
|
+
endpoint: data.endpoint || SITE + "/v1/ask",
|
|
23
|
+
// A short slug naming the integration that sends the calls (a plugin or package name).
|
|
24
|
+
via: data.via || "",
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
class OneTagError extends Error {
|
|
28
|
+
constructor(code, message, status = 0) {
|
|
29
|
+
super(message);
|
|
30
|
+
this.name = "OneTagError";
|
|
31
|
+
this.code = code;
|
|
32
|
+
this.status = status;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const isA = (value, type) => typeof globalThis[type] === "function" && value instanceof globalThis[type];
|
|
37
|
+
const textPart = (text) => ({ type: "text", text: String(text) });
|
|
38
|
+
const len = (s) => (typeof s === "string" ? s.length : 0);
|
|
39
|
+
|
|
40
|
+
// Trim the text parts, in order, to the characters the server has left after the
|
|
41
|
+
// prompt, system and history (it rejects the whole call over MAX_TEXT, counting 7 per
|
|
42
|
+
// separator between parts). A cut never lands between the two halves of a surrogate
|
|
43
|
+
// pair; parts trimmed to nothing are dropped.
|
|
44
|
+
function fit(parts, budget) {
|
|
45
|
+
const out = [];
|
|
46
|
+
let sep = 0;
|
|
47
|
+
for (const part of parts) {
|
|
48
|
+
if (part.type !== "text") {
|
|
49
|
+
out.push(part);
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
let cut = Math.max(0, Math.min(budget - sep, part.text.length));
|
|
53
|
+
const c = part.text.charCodeAt(cut - 1);
|
|
54
|
+
if (cut < part.text.length && c >= 0xd800 && c < 0xdc00) cut--;
|
|
55
|
+
if (!cut) continue;
|
|
56
|
+
out.push({ type: "text", text: part.text.slice(0, cut) });
|
|
57
|
+
budget -= sep + cut;
|
|
58
|
+
sep = 7;
|
|
59
|
+
}
|
|
60
|
+
return out;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function configure(options = {}) {
|
|
64
|
+
if (options.key !== undefined) config.key = options.key;
|
|
65
|
+
if (options.endpoint !== undefined) config.endpoint = options.endpoint;
|
|
66
|
+
if (options.via !== undefined) config.via = options.via;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const isSvg = (s) => /^(data:)?image\/svg|\.svg([?#]|$)/i.test(s || "");
|
|
70
|
+
|
|
71
|
+
// Draw onto a canvas no larger than MAX_SIDE: JPEG on white for photos, PNG for SVG.
|
|
72
|
+
function rasterize(source, width, height, svg) {
|
|
73
|
+
const scale = Math.min(1, MAX_SIDE / Math.max(width, height));
|
|
74
|
+
const canvas = document.createElement("canvas");
|
|
75
|
+
canvas.width = Math.round(width * scale) || 1;
|
|
76
|
+
canvas.height = Math.round(height * scale) || 1;
|
|
77
|
+
const g = canvas.getContext("2d");
|
|
78
|
+
if (!svg) {
|
|
79
|
+
g.fillStyle = "#fff";
|
|
80
|
+
g.fillRect(0, 0, canvas.width, canvas.height);
|
|
81
|
+
}
|
|
82
|
+
g.drawImage(source, 0, 0, canvas.width, canvas.height);
|
|
83
|
+
return { type: "image", data_url: svg ? canvas.toDataURL("image/png") : canvas.toDataURL("image/jpeg", 0.85) };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async function fromImg(img, svg) {
|
|
87
|
+
if (!img.naturalWidth) await img.decode();
|
|
88
|
+
return rasterize(img, img.naturalWidth, img.naturalHeight, svg);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async function imagePart(input) {
|
|
92
|
+
try {
|
|
93
|
+
if (isA(input, "HTMLImageElement")) return await fromImg(input, isSvg(input.currentSrc || input.src));
|
|
94
|
+
const svg = isSvg(input.type);
|
|
95
|
+
if (!svg && typeof globalThis.createImageBitmap === "function") {
|
|
96
|
+
const bitmap = await globalThis.createImageBitmap(input);
|
|
97
|
+
try {
|
|
98
|
+
return rasterize(bitmap, bitmap.width, bitmap.height, false);
|
|
99
|
+
} finally {
|
|
100
|
+
bitmap.close();
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
const url = URL.createObjectURL(input);
|
|
104
|
+
try {
|
|
105
|
+
const img = new Image();
|
|
106
|
+
img.src = url;
|
|
107
|
+
return await fromImg(img, svg);
|
|
108
|
+
} finally {
|
|
109
|
+
URL.revokeObjectURL(url);
|
|
110
|
+
}
|
|
111
|
+
} catch (e) {
|
|
112
|
+
throw new OneTagError("bad_input", e.name === "SecurityError"
|
|
113
|
+
? "Cross-origin image without CORS: add crossorigin=\"anonymous\" or pass a File"
|
|
114
|
+
: "Could not read image: " + e.message);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Every field as a "name: value" line, except password fields.
|
|
119
|
+
function formText(form) {
|
|
120
|
+
const secret = new Set();
|
|
121
|
+
for (const el of form.elements || []) if (el.type === "password") secret.add(el.name);
|
|
122
|
+
const lines = [];
|
|
123
|
+
for (const [name, value] of new FormData(form)) {
|
|
124
|
+
if (!secret.has(name)) lines.push(name + ": " + (typeof value === "string" ? value : value.name));
|
|
125
|
+
}
|
|
126
|
+
return lines.join("\n");
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async function normalizeInput(input) {
|
|
130
|
+
if (input == null) return [];
|
|
131
|
+
if (Array.isArray(input)) return (await Promise.all(input.map(normalizeInput))).flat();
|
|
132
|
+
if (typeof input === "string") return [textPart(input)];
|
|
133
|
+
if (isA(input, "HTMLImageElement") || (isA(input, "Blob") && /^image\//.test(input.type))) return [await imagePart(input)];
|
|
134
|
+
if (isA(input, "Blob")) return [textPart(await input.text())];
|
|
135
|
+
if (isA(input, "HTMLFormElement")) return [textPart(formText(input))];
|
|
136
|
+
if (CONTROLS.some((type) => isA(input, type))) return [textPart(input.value)];
|
|
137
|
+
if (isA(input, "HTMLElement")) return [textPart(input.innerText || input.textContent || "")];
|
|
138
|
+
// NodeList, HTMLCollection, Set: read like an array. After the element checks,
|
|
139
|
+
// because forms and selects are iterable too.
|
|
140
|
+
if (typeof input[Symbol.iterator] === "function") return normalizeInput(Array.from(input));
|
|
141
|
+
let json;
|
|
142
|
+
try {
|
|
143
|
+
json = JSON.stringify(input, null, 2);
|
|
144
|
+
} catch (e) {}
|
|
145
|
+
if (json === undefined) throw new OneTagError("bad_input", "Unsupported input: " + typeof input);
|
|
146
|
+
return [textPart(json)];
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function parseSSE(buffer) {
|
|
150
|
+
const blocks = buffer.split("\n\n");
|
|
151
|
+
const rest = blocks.pop();
|
|
152
|
+
const events = [];
|
|
153
|
+
for (const block of blocks) {
|
|
154
|
+
for (const line of block.split("\n")) {
|
|
155
|
+
if (line.startsWith("data: ")) events.push(JSON.parse(line.slice(6)));
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return { events, rest };
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const stripFences = (s) => s.trim().replace(/^```(?:json)?\s*/i, "").replace(/\s*```$/, "");
|
|
162
|
+
|
|
163
|
+
const networkError = (detail) => new OneTagError("network", "Network error: " + detail, 0);
|
|
164
|
+
// A fired signal rejects fetch with its reason, which is an AbortError unless the caller gave one.
|
|
165
|
+
const isAbort = (e, signal) => !!(signal && signal.aborted) || !!(e && e.name === "AbortError");
|
|
166
|
+
const abortError = () => new OneTagError("aborted", "Request aborted", 0);
|
|
167
|
+
|
|
168
|
+
async function readStream(response, onDelta, signal) {
|
|
169
|
+
const reader = response.body.getReader();
|
|
170
|
+
const decoder = new globalThis.TextDecoder();
|
|
171
|
+
let buffer = "";
|
|
172
|
+
let text = "";
|
|
173
|
+
try {
|
|
174
|
+
for (;;) {
|
|
175
|
+
const { done, value } = await reader.read().catch((e) => { throw isAbort(e, signal) ? abortError() : networkError(e.message); });
|
|
176
|
+
if (done) throw networkError("the stream ended before the answer was complete");
|
|
177
|
+
const parsed = parseSSE(buffer + decoder.decode(value, { stream: true }));
|
|
178
|
+
buffer = parsed.rest;
|
|
179
|
+
for (const event of parsed.events) {
|
|
180
|
+
if (event.error) throw new OneTagError(event.error.code, event.error.message, response.status);
|
|
181
|
+
if (event.done) return text;
|
|
182
|
+
if (event.delta) {
|
|
183
|
+
text += event.delta;
|
|
184
|
+
if (typeof onDelta === "function") onDelta(event.delta);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
} finally {
|
|
189
|
+
reader.cancel().catch(() => {});
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
async function ask(prompt, input, options) {
|
|
194
|
+
const o = options || {};
|
|
195
|
+
// options.key and options.via are for packages built on the SDK: they send their own
|
|
196
|
+
// key and name with each call and leave the page's configuration alone.
|
|
197
|
+
const key = o.key || config.key;
|
|
198
|
+
if (!key || /^YOUR_/i.test(key)) {
|
|
199
|
+
// A placeholder left by a template or an AI coding tool is reported like a missing key, with where to fix it.
|
|
200
|
+
throw new OneTagError("no_key", (key ? "OneTag AI key is still the placeholder " + key + ". " : "No OneTag AI key. ")
|
|
201
|
+
+ "Get a free key (email only) at " + HOME + "/#get-key and put it in data-key on the script tag (or onetag.configure({key})).", 0);
|
|
202
|
+
}
|
|
203
|
+
const history = Array.isArray(o.history) ? o.history : [];
|
|
204
|
+
// 20 per history message: the server's allowance for its role and framing.
|
|
205
|
+
const budget = MAX_TEXT - len(prompt) - len(o.system) - history.reduce((n, m) => n + 20 + len(m && m.content), 0);
|
|
206
|
+
const body = JSON.stringify({
|
|
207
|
+
key,
|
|
208
|
+
prompt,
|
|
209
|
+
input: fit(await normalizeInput(input), budget),
|
|
210
|
+
system: o.system,
|
|
211
|
+
json: !!o.json,
|
|
212
|
+
max_tokens: o.maxTokens,
|
|
213
|
+
stream: !!o.stream,
|
|
214
|
+
history: o.history,
|
|
215
|
+
via: o.via || config.via || undefined,
|
|
216
|
+
});
|
|
217
|
+
let response;
|
|
218
|
+
try {
|
|
219
|
+
response = await globalThis.fetch(config.endpoint, { method: "POST", headers: { "Content-Type": "application/json" }, body, signal: o.signal });
|
|
220
|
+
} catch (e) {
|
|
221
|
+
throw isAbort(e, o.signal) ? abortError() : networkError(e && e.message);
|
|
222
|
+
}
|
|
223
|
+
if (!response.ok) {
|
|
224
|
+
const error = (await response.json().catch(() => null) || {}).error;
|
|
225
|
+
throw error
|
|
226
|
+
? new OneTagError(error.code, error.message, response.status)
|
|
227
|
+
: new OneTagError("upstream", "OneTag AI returned HTTP " + response.status, response.status);
|
|
228
|
+
}
|
|
229
|
+
const text = o.stream
|
|
230
|
+
? await readStream(response, o.stream, o.signal)
|
|
231
|
+
: (await response.json().catch((e) => { throw isAbort(e, o.signal) ? abortError() : e; })).text;
|
|
232
|
+
if (!o.json) return text;
|
|
233
|
+
try {
|
|
234
|
+
return JSON.parse(stripFences(text));
|
|
235
|
+
} catch (e) {
|
|
236
|
+
throw new OneTagError("bad_json", "The answer was not valid JSON", 0);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const version = "1.2.0";
|
|
241
|
+
|
|
242
|
+
export { ask, configure, OneTagError, version };
|
package/index.d.ts
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/** A piece of input: text, a DOM element (its text), a form (its fields), an image or File, any JSON value, or a list of those. */
|
|
2
|
+
export type AskInput = string | Blob | Element | Iterable<unknown> | object | null | undefined;
|
|
3
|
+
|
|
4
|
+
export interface AskMessage {
|
|
5
|
+
role: "user" | "assistant";
|
|
6
|
+
content: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface AskOptions {
|
|
10
|
+
/** Instructions for the model: a persona, limits, a format. */
|
|
11
|
+
system?: string;
|
|
12
|
+
/** Called with each piece of the answer as it arrives. The promise still resolves with the whole answer. */
|
|
13
|
+
stream?: (delta: string) => void;
|
|
14
|
+
/** Parse the answer as JSON and return the value. */
|
|
15
|
+
json?: boolean;
|
|
16
|
+
/** Cap on the answer length in tokens (default 500, at most 10000). */
|
|
17
|
+
maxTokens?: number;
|
|
18
|
+
/** Earlier turns of the conversation, oldest first. */
|
|
19
|
+
history?: AskMessage[];
|
|
20
|
+
/** Abort the call. */
|
|
21
|
+
signal?: AbortSignal;
|
|
22
|
+
/** Use this key for this call instead of the configured one. */
|
|
23
|
+
key?: string;
|
|
24
|
+
/** A short slug naming the integration that sends the call. For packages built on this one. */
|
|
25
|
+
via?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface ConfigureOptions {
|
|
29
|
+
/** Your OneTag AI key. It is designed to be public, like an analytics ID. */
|
|
30
|
+
key?: string;
|
|
31
|
+
/** The API endpoint. Default https://onetagai.com/v1/ask */
|
|
32
|
+
endpoint?: string;
|
|
33
|
+
/** A short slug naming the integration that sends the calls. */
|
|
34
|
+
via?: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export type OneTagErrorCode =
|
|
38
|
+
| "no_key"
|
|
39
|
+
| "bad_input"
|
|
40
|
+
| "bad_json"
|
|
41
|
+
| "network"
|
|
42
|
+
| "aborted"
|
|
43
|
+
| "bad_request"
|
|
44
|
+
| "bad_key"
|
|
45
|
+
| "domain_not_allowed"
|
|
46
|
+
| "too_large"
|
|
47
|
+
| "quota_exceeded"
|
|
48
|
+
| "rate_limited"
|
|
49
|
+
| "upstream"
|
|
50
|
+
| "paused"
|
|
51
|
+
| (string & {});
|
|
52
|
+
|
|
53
|
+
export class OneTagError extends Error {
|
|
54
|
+
code: OneTagErrorCode;
|
|
55
|
+
/** HTTP status, or 0 when the call never reached the server. */
|
|
56
|
+
status: number;
|
|
57
|
+
constructor(code: OneTagErrorCode, message: string, status?: number);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Set the key once, before the first call. */
|
|
61
|
+
export function configure(options?: ConfigureOptions): void;
|
|
62
|
+
|
|
63
|
+
/** Ask the model. `input` is what the prompt is about: text, an element, a form, an image, JSON, or a list of those. */
|
|
64
|
+
export function ask<T = unknown>(prompt: string, input: AskInput, options: AskOptions & { json: true }): Promise<T>;
|
|
65
|
+
export function ask(prompt: string, input?: AskInput, options?: AskOptions): Promise<string>;
|
|
66
|
+
|
|
67
|
+
export const version: string;
|
|
68
|
+
/** The shared demo key this package falls back to on localhost when no key is set. */
|
|
69
|
+
export const DEMO_KEY: string;
|
|
70
|
+
|
|
71
|
+
declare const onetag: {
|
|
72
|
+
ask: typeof ask;
|
|
73
|
+
configure: typeof configure;
|
|
74
|
+
OneTagError: typeof OneTagError;
|
|
75
|
+
version: string;
|
|
76
|
+
DEMO_KEY: string;
|
|
77
|
+
};
|
|
78
|
+
export default onetag;
|
package/index.js
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/*! onetag | https://onetagai.com | MIT */
|
|
2
|
+
import { ask as send, configure as set, OneTagError, version } from "./core.js";
|
|
3
|
+
|
|
4
|
+
// The shared demo key. It answers while you develop on localhost, so a fresh install works
|
|
5
|
+
// before you have a key. Its small daily budget is shared by everyone, so a deployed site
|
|
6
|
+
// needs its own free key.
|
|
7
|
+
const DEMO_KEY = "ot_demo000000000000000000000";
|
|
8
|
+
const GET_KEY =
|
|
9
|
+
"Get your own free key (email only): curl -X POST https://onetagai.com/api/signup " +
|
|
10
|
+
`-H "Content-Type: application/json" -d '{"email":"you@example.com"}'`;
|
|
11
|
+
|
|
12
|
+
let key = "";
|
|
13
|
+
let via = "npm";
|
|
14
|
+
let told = false;
|
|
15
|
+
|
|
16
|
+
const missing = (value) => !value || /^YOUR_/i.test(value);
|
|
17
|
+
|
|
18
|
+
// A page on localhost or opened from a file; outside a browser, anything but a production build.
|
|
19
|
+
function isLocal() {
|
|
20
|
+
const location = globalThis.location;
|
|
21
|
+
if (location && typeof location.hostname === "string") {
|
|
22
|
+
return /^(|localhost|127\.0\.0\.1|\[::1\]|.+\.localhost)$/.test(location.hostname);
|
|
23
|
+
}
|
|
24
|
+
const env = globalThis.process && globalThis.process.env;
|
|
25
|
+
return !!env && env.NODE_ENV !== "production";
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function configure(options = {}) {
|
|
29
|
+
if (options.key !== undefined) key = options.key;
|
|
30
|
+
if (options.via !== undefined) via = options.via;
|
|
31
|
+
if (options.endpoint !== undefined) set({ endpoint: options.endpoint });
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function ask(prompt, input, options) {
|
|
35
|
+
const o = options || {};
|
|
36
|
+
let use = o.key || key;
|
|
37
|
+
if (missing(use)) {
|
|
38
|
+
if (!isLocal()) {
|
|
39
|
+
throw new OneTagError("no_key", "No OneTag AI key: the demo key only answers on localhost. " + GET_KEY + " Then call configure({ key }).", 0);
|
|
40
|
+
}
|
|
41
|
+
if (!told) {
|
|
42
|
+
told = true;
|
|
43
|
+
console.info("[onetag] No key set: using the shared demo key, which works on localhost only. " + GET_KEY);
|
|
44
|
+
}
|
|
45
|
+
use = DEMO_KEY;
|
|
46
|
+
}
|
|
47
|
+
return send(prompt, input, { ...o, key: use, via: o.via || via });
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const onetag = { ask, configure, OneTagError, version, DEMO_KEY };
|
|
51
|
+
|
|
52
|
+
export { ask, configure, OneTagError, version, DEMO_KEY };
|
|
53
|
+
export default onetag;
|
package/package.json
CHANGED
|
@@ -1,6 +1,32 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "onetagai",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "1.2.0",
|
|
4
|
+
"description": "AI for static sites without a backend: ask(prompt, input) from the browser with a public key. No server, no secret in your code.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./index.js",
|
|
7
|
+
"module": "./index.js",
|
|
8
|
+
"types": "./index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./index.d.ts",
|
|
12
|
+
"default": "./index.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"files": ["index.js", "core.js", "index.d.ts", "README.md", "LICENSE"],
|
|
16
|
+
"sideEffects": false,
|
|
17
|
+
"scripts": {
|
|
18
|
+
"build": "node build.mjs",
|
|
19
|
+
"test": "node build.mjs --check && node --test test/*.test.mjs",
|
|
20
|
+
"prepublishOnly": "npm test"
|
|
21
|
+
},
|
|
22
|
+
"keywords": ["ai", "llm", "static-site", "no-backend", "browser", "frontend", "chatbot", "summarize", "translate", "jamstack", "onetag", "onetagai"],
|
|
23
|
+
"license": "MIT",
|
|
24
|
+
"author": "OneTag AI (https://onetagai.com)",
|
|
25
|
+
"homepage": "https://onetagai.com/?ref=npm",
|
|
26
|
+
"repository": {
|
|
27
|
+
"type": "git",
|
|
28
|
+
"url": "git+https://github.com/fengyiqicoder/onetagai.git",
|
|
29
|
+
"directory": "packages/onetag"
|
|
30
|
+
},
|
|
31
|
+
"bugs": "https://github.com/fengyiqicoder/onetagai/issues"
|
|
32
|
+
}
|