@adriangalilea/utils 0.4.1 → 0.6.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/README.md +99 -19
- package/dist/bot/access-control.d.ts +339 -0
- package/dist/bot/access-control.d.ts.map +1 -0
- package/dist/bot/access-control.js +516 -0
- package/dist/bot/access-control.js.map +1 -0
- package/dist/bot/index.d.ts +17 -0
- package/dist/bot/index.d.ts.map +1 -0
- package/dist/bot/index.js +17 -0
- package/dist/bot/index.js.map +1 -0
- package/dist/bot/kit.d.ts +50 -0
- package/dist/bot/kit.d.ts.map +1 -0
- package/dist/bot/kit.js +52 -0
- package/dist/bot/kit.js.map +1 -0
- package/dist/bot/llm-stream.d.ts +84 -0
- package/dist/bot/llm-stream.d.ts.map +1 -0
- package/dist/bot/llm-stream.js +201 -0
- package/dist/bot/llm-stream.js.map +1 -0
- package/dist/offensive.d.ts +89 -17
- package/dist/offensive.d.ts.map +1 -1
- package/dist/offensive.js +103 -17
- package/dist/offensive.js.map +1 -1
- package/package.json +47 -2
package/dist/bot/kit.js
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { Plugin } from 'gramio';
|
|
2
|
+
import { kev } from '../platform/kev.js';
|
|
3
|
+
export const gracefulStart = async (bot, opts = {}) => {
|
|
4
|
+
const log = opts.log === false ? () => { } : (opts.log ?? ((m) => console.log(m)));
|
|
5
|
+
const forceMs = opts.forceExitAfterMs ?? 10_000;
|
|
6
|
+
let stopping = false;
|
|
7
|
+
const stop = async (signal) => {
|
|
8
|
+
if (stopping)
|
|
9
|
+
return;
|
|
10
|
+
stopping = true;
|
|
11
|
+
log(`[bot] ${signal} received, shutting down…`);
|
|
12
|
+
const force = setTimeout(() => {
|
|
13
|
+
console.error(`[bot] forced exit after ${forceMs}ms`);
|
|
14
|
+
process.exit(1);
|
|
15
|
+
}, forceMs);
|
|
16
|
+
force.unref?.();
|
|
17
|
+
try {
|
|
18
|
+
await bot.stop();
|
|
19
|
+
await opts.onShutdown?.();
|
|
20
|
+
log('[bot] shutdown clean');
|
|
21
|
+
}
|
|
22
|
+
catch (e) {
|
|
23
|
+
console.error('[bot] shutdown error', e);
|
|
24
|
+
}
|
|
25
|
+
finally {
|
|
26
|
+
clearTimeout(force);
|
|
27
|
+
process.exit(opts.exitCode ?? 0);
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
process.on('SIGINT', () => void stop('SIGINT'));
|
|
31
|
+
process.on('SIGTERM', () => void stop('SIGTERM'));
|
|
32
|
+
await bot.start();
|
|
33
|
+
};
|
|
34
|
+
export const adminContext = (opts = {}) => {
|
|
35
|
+
// KEV resolves: process.env → .env (project + monorepo, auto-discovered) → fallback.
|
|
36
|
+
// Cached after first read. `kev.int` panics on non-int strings, so a malformed
|
|
37
|
+
// env var screams immediately rather than producing NaN downstream.
|
|
38
|
+
const adminId = kev.int('TELEGRAM_ADMIN_ID', opts.adminId ?? 0);
|
|
39
|
+
if (!adminId) {
|
|
40
|
+
throw new Error('adminContext: TELEGRAM_ADMIN_ID not set and no adminId fallback. ' +
|
|
41
|
+
'Get your Telegram id from @UserIDentifyBot.');
|
|
42
|
+
}
|
|
43
|
+
return new Plugin('@adriangalilea/utils/bot/admin')
|
|
44
|
+
.decorate({ adminId })
|
|
45
|
+
.derive((ctx) => ({
|
|
46
|
+
// `senderId` is provided by gramio's SenderMixin. It's `undefined` on
|
|
47
|
+
// service-style events without an actor; the strict equality below
|
|
48
|
+
// gives `false` in that case, which is the right answer.
|
|
49
|
+
isAdmin: 'senderId' in ctx && ctx.senderId === adminId,
|
|
50
|
+
}));
|
|
51
|
+
};
|
|
52
|
+
//# sourceMappingURL=kit.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"kit.js","sourceRoot":"","sources":["../../src/bot/kit.ts"],"names":[],"mappings":"AAwBA,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAA;AAC/B,OAAO,EAAE,GAAG,EAAE,MAAM,oBAAoB,CAAA;AAexC,MAAM,CAAC,MAAM,aAAa,GAAG,KAAK,EAChC,GAAW,EACX,OAA6B,EAAE,EAChB,EAAE;IACjB,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,KAAK,KAAK,CAAC,CAAC,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;IACjF,MAAM,OAAO,GAAG,IAAI,CAAC,gBAAgB,IAAI,MAAM,CAAA;IAE/C,IAAI,QAAQ,GAAG,KAAK,CAAA;IAEpB,MAAM,IAAI,GAAG,KAAK,EAAE,MAAc,EAAE,EAAE;QACpC,IAAI,QAAQ;YAAE,OAAM;QACpB,QAAQ,GAAG,IAAI,CAAA;QACf,GAAG,CAAC,SAAS,MAAM,2BAA2B,CAAC,CAAA;QAE/C,MAAM,KAAK,GAAG,UAAU,CAAC,GAAG,EAAE;YAC5B,OAAO,CAAC,KAAK,CAAC,2BAA2B,OAAO,IAAI,CAAC,CAAA;YACrD,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;QACjB,CAAC,EAAE,OAAO,CAAC,CAAA;QACX,KAAK,CAAC,KAAK,EAAE,EAAE,CAAA;QAEf,IAAI,CAAC;YACH,MAAM,GAAG,CAAC,IAAI,EAAE,CAAA;YAChB,MAAM,IAAI,CAAC,UAAU,EAAE,EAAE,CAAA;YACzB,GAAG,CAAC,sBAAsB,CAAC,CAAA;QAC7B,CAAC;QAAC,OAAO,CAAC,EAAE,CAAC;YACX,OAAO,CAAC,KAAK,CAAC,sBAAsB,EAAE,CAAC,CAAC,CAAA;QAC1C,CAAC;gBAAS,CAAC;YACT,YAAY,CAAC,KAAK,CAAC,CAAA;YACnB,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,IAAI,CAAC,CAAC,CAAA;QAClC,CAAC;IACH,CAAC,CAAA;IAED,OAAO,CAAC,EAAE,CAAC,QAAQ,EAAE,GAAG,EAAE,CAAC,KAAK,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAA;IAC/C,OAAO,CAAC,EAAE,CAAC,SAAS,EAAE,GAAG,EAAE,CAAC,KAAK,IAAI,CAAC,SAAS,CAAC,CAAC,CAAA;IAEjD,MAAM,GAAG,CAAC,KAAK,EAAE,CAAA;AACnB,CAAC,CAAA;AASD,MAAM,CAAC,MAAM,YAAY,GAAG,CAAC,OAA4B,EAAE,EAAE,EAAE;IAC7D,qFAAqF;IACrF,+EAA+E;IAC/E,oEAAoE;IACpE,MAAM,OAAO,GAAG,GAAG,CAAC,GAAG,CAAC,mBAAmB,EAAE,IAAI,CAAC,OAAO,IAAI,CAAC,CAAC,CAAA;IAE/D,IAAI,CAAC,OAAO,EAAE,CAAC;QACb,MAAM,IAAI,KAAK,CACb,mEAAmE;YACjE,6CAA6C,CAChD,CAAA;IACH,CAAC;IAED,OAAO,IAAI,MAAM,CAAC,gCAAgC,CAAC;SAChD,QAAQ,CAAC,EAAE,OAAO,EAAE,CAAC;SACrB,MAAM,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC;QAChB,sEAAsE;QACtE,mEAAmE;QACnE,yDAAyD;QACzD,OAAO,EAAE,UAAU,IAAI,GAAG,IAAI,GAAG,CAAC,QAAQ,KAAK,OAAO;KACvD,CAAC,CAAC,CAAA;AACP,CAAC,CAAA"}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LLM streaming output for GramIO bots.
|
|
3
|
+
*
|
|
4
|
+
* Sends a placeholder, then debounces `editMessageText` calls as the
|
|
5
|
+
* LLM produces chunks. Markdown is parsed locally with
|
|
6
|
+
* `markdownToFormattable` — invalid markup degrades to plain text
|
|
7
|
+
* instead of failing (Telegram's `parse_mode` would reject the whole
|
|
8
|
+
* message). Splits at 4000 chars by promoting the next chunk to a
|
|
9
|
+
* fresh message at a paragraph/line/word boundary.
|
|
10
|
+
*
|
|
11
|
+
* Peer deps: `gramio`, `@gramio/format`, `marked`.
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* import { Bot } from 'gramio'
|
|
15
|
+
* import { llmStream } from '@adriangalilea/utils/bot/llm-stream'
|
|
16
|
+
* import Anthropic from '@anthropic-ai/sdk'
|
|
17
|
+
*
|
|
18
|
+
* const claude = new Anthropic()
|
|
19
|
+
* const bot = new Bot(process.env.BOT_TOKEN!)
|
|
20
|
+
* .extend(llmStream())
|
|
21
|
+
* .on('message', async (ctx) => {
|
|
22
|
+
* const stream = ctx.startStream()
|
|
23
|
+
* const sse = await claude.messages.stream({
|
|
24
|
+
* model: 'claude-opus-4-5',
|
|
25
|
+
* max_tokens: 1024,
|
|
26
|
+
* messages: [{ role: 'user', content: ctx.text ?? '' }],
|
|
27
|
+
* })
|
|
28
|
+
* for await (const chunk of sse) {
|
|
29
|
+
* if (chunk.type === 'content_block_delta' && chunk.delta.type === 'text_delta') {
|
|
30
|
+
* await stream.append(chunk.delta.text)
|
|
31
|
+
* }
|
|
32
|
+
* }
|
|
33
|
+
* await stream.end()
|
|
34
|
+
* })
|
|
35
|
+
*
|
|
36
|
+
* bot.start()
|
|
37
|
+
*/
|
|
38
|
+
import { Plugin } from 'gramio';
|
|
39
|
+
export type StreamOptions = {
|
|
40
|
+
/** Debounce window between edits, in ms. Default 800. */
|
|
41
|
+
debounceMs?: number;
|
|
42
|
+
/** Initial placeholder shown until the first chunk arrives. Default "…". */
|
|
43
|
+
placeholder?: string;
|
|
44
|
+
/** Parse buffer as markdown. Default true. Set false for plain text streaming. */
|
|
45
|
+
markdown?: boolean;
|
|
46
|
+
/** Called on edit/send errors after internal recovery (rate limits, etc.). */
|
|
47
|
+
onError?: (err: unknown) => void;
|
|
48
|
+
};
|
|
49
|
+
export declare class MarkdownStreamer {
|
|
50
|
+
private buffer;
|
|
51
|
+
private currentMessageId?;
|
|
52
|
+
private firstSendPromise?;
|
|
53
|
+
private debounceTimer?;
|
|
54
|
+
private inFlight;
|
|
55
|
+
private dirty;
|
|
56
|
+
private ended;
|
|
57
|
+
private chatId;
|
|
58
|
+
private bot;
|
|
59
|
+
private opts;
|
|
60
|
+
constructor(ctx: {
|
|
61
|
+
chat: {
|
|
62
|
+
id: number;
|
|
63
|
+
};
|
|
64
|
+
bot: MarkdownStreamer['bot'];
|
|
65
|
+
}, opts: StreamOptions);
|
|
66
|
+
/** Append a chunk. Schedules a debounced edit. */
|
|
67
|
+
append(text: string): Promise<void>;
|
|
68
|
+
/** Flush any pending edit and close the stream. Idempotent. */
|
|
69
|
+
end(): Promise<void>;
|
|
70
|
+
private scheduleFlush;
|
|
71
|
+
private flushNow;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* GramIO plugin. Adds `ctx.startStream(opts?)` on every message context.
|
|
75
|
+
*
|
|
76
|
+
* Defaults set here apply to every stream; per-call options in
|
|
77
|
+
* `ctx.startStream({...})` override them.
|
|
78
|
+
*/
|
|
79
|
+
export declare const llmStream: (defaults?: StreamOptions) => Plugin<{}, import("gramio").DeriveDefinitions & {
|
|
80
|
+
message: {
|
|
81
|
+
startStream: (opts?: StreamOptions) => MarkdownStreamer;
|
|
82
|
+
};
|
|
83
|
+
}, {}>;
|
|
84
|
+
//# sourceMappingURL=llm-stream.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"llm-stream.d.ts","sourceRoot":"","sources":["../../src/bot/llm-stream.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AACH,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAA;AAM/B,MAAM,MAAM,aAAa,GAAG;IAC1B,yDAAyD;IACzD,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,4EAA4E;IAC5E,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,kFAAkF;IAClF,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,8EAA8E;IAC9E,OAAO,CAAC,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,IAAI,CAAA;CACjC,CAAA;AAED,qBAAa,gBAAgB;IAC3B,OAAO,CAAC,MAAM,CAAK;IACnB,OAAO,CAAC,gBAAgB,CAAC,CAAQ;IACjC,OAAO,CAAC,gBAAgB,CAAC,CAAe;IACxC,OAAO,CAAC,aAAa,CAAC,CAA+B;IACrD,OAAO,CAAC,QAAQ,CAAQ;IACxB,OAAO,CAAC,KAAK,CAAQ;IACrB,OAAO,CAAC,KAAK,CAAQ;IAErB,OAAO,CAAC,MAAM,CAAQ;IAItB,OAAO,CAAC,GAAG,CAYV;IACD,OAAO,CAAC,IAAI,CAAyB;gBAGnC,GAAG,EAAE;QAAE,IAAI,EAAE;YAAE,EAAE,EAAE,MAAM,CAAA;SAAE,CAAC;QAAC,GAAG,EAAE,gBAAgB,CAAC,KAAK,CAAC,CAAA;KAAE,EAC3D,IAAI,EAAE,aAAa;IAYrB,kDAAkD;IAC5C,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IA8CzC,+DAA+D;IACzD,GAAG,IAAI,OAAO,CAAC,IAAI,CAAC;IAW1B,OAAO,CAAC,aAAa;YAaP,QAAQ;CAmCvB;AAED;;;;;GAKG;AACH,eAAO,MAAM,SAAS,GAAI,WAAU,aAAkB;;6BAI9B,aAAa;;MAEhC,CAAA"}
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LLM streaming output for GramIO bots.
|
|
3
|
+
*
|
|
4
|
+
* Sends a placeholder, then debounces `editMessageText` calls as the
|
|
5
|
+
* LLM produces chunks. Markdown is parsed locally with
|
|
6
|
+
* `markdownToFormattable` — invalid markup degrades to plain text
|
|
7
|
+
* instead of failing (Telegram's `parse_mode` would reject the whole
|
|
8
|
+
* message). Splits at 4000 chars by promoting the next chunk to a
|
|
9
|
+
* fresh message at a paragraph/line/word boundary.
|
|
10
|
+
*
|
|
11
|
+
* Peer deps: `gramio`, `@gramio/format`, `marked`.
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* import { Bot } from 'gramio'
|
|
15
|
+
* import { llmStream } from '@adriangalilea/utils/bot/llm-stream'
|
|
16
|
+
* import Anthropic from '@anthropic-ai/sdk'
|
|
17
|
+
*
|
|
18
|
+
* const claude = new Anthropic()
|
|
19
|
+
* const bot = new Bot(process.env.BOT_TOKEN!)
|
|
20
|
+
* .extend(llmStream())
|
|
21
|
+
* .on('message', async (ctx) => {
|
|
22
|
+
* const stream = ctx.startStream()
|
|
23
|
+
* const sse = await claude.messages.stream({
|
|
24
|
+
* model: 'claude-opus-4-5',
|
|
25
|
+
* max_tokens: 1024,
|
|
26
|
+
* messages: [{ role: 'user', content: ctx.text ?? '' }],
|
|
27
|
+
* })
|
|
28
|
+
* for await (const chunk of sse) {
|
|
29
|
+
* if (chunk.type === 'content_block_delta' && chunk.delta.type === 'text_delta') {
|
|
30
|
+
* await stream.append(chunk.delta.text)
|
|
31
|
+
* }
|
|
32
|
+
* }
|
|
33
|
+
* await stream.end()
|
|
34
|
+
* })
|
|
35
|
+
*
|
|
36
|
+
* bot.start()
|
|
37
|
+
*/
|
|
38
|
+
import { Plugin } from 'gramio';
|
|
39
|
+
import { markdownToFormattable } from '@gramio/format/markdown';
|
|
40
|
+
const MAX_LEN = 4000; // Telegram caps at 4096; leave headroom for entity offsets
|
|
41
|
+
const DEFAULT_DEBOUNCE_MS = 800;
|
|
42
|
+
export class MarkdownStreamer {
|
|
43
|
+
buffer = '';
|
|
44
|
+
currentMessageId;
|
|
45
|
+
firstSendPromise;
|
|
46
|
+
debounceTimer;
|
|
47
|
+
inFlight = false;
|
|
48
|
+
dirty = false;
|
|
49
|
+
ended = false;
|
|
50
|
+
chatId;
|
|
51
|
+
// Match gramio's `bot.api` shape structurally. `text` accepts string or any
|
|
52
|
+
// `Formattable` (from `@gramio/format`) — both stringify safely. We don't
|
|
53
|
+
// import gramio's full Bot type to keep the streamer testable in isolation.
|
|
54
|
+
bot;
|
|
55
|
+
opts;
|
|
56
|
+
constructor(ctx, opts) {
|
|
57
|
+
this.chatId = ctx.chat.id;
|
|
58
|
+
this.bot = ctx.bot;
|
|
59
|
+
this.opts = {
|
|
60
|
+
debounceMs: opts.debounceMs ?? DEFAULT_DEBOUNCE_MS,
|
|
61
|
+
placeholder: opts.placeholder ?? '…',
|
|
62
|
+
markdown: opts.markdown ?? true,
|
|
63
|
+
onError: opts.onError ?? ((e) => console.error('[llm-stream]', e)),
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
/** Append a chunk. Schedules a debounced edit. */
|
|
67
|
+
async append(text) {
|
|
68
|
+
if (this.ended)
|
|
69
|
+
throw new Error('stream already ended');
|
|
70
|
+
if (!text)
|
|
71
|
+
return;
|
|
72
|
+
// first chunk: send the placeholder so we have a message_id to edit.
|
|
73
|
+
// Serialized via firstSendPromise so concurrent appends don't double-send.
|
|
74
|
+
if (this.currentMessageId === undefined && !this.firstSendPromise) {
|
|
75
|
+
this.firstSendPromise = (async () => {
|
|
76
|
+
const sent = await this.bot.api.sendMessage({
|
|
77
|
+
chat_id: this.chatId,
|
|
78
|
+
text: this.opts.placeholder,
|
|
79
|
+
});
|
|
80
|
+
this.currentMessageId = sent.message_id;
|
|
81
|
+
})();
|
|
82
|
+
}
|
|
83
|
+
if (this.firstSendPromise)
|
|
84
|
+
await this.firstSendPromise;
|
|
85
|
+
// overflow: freeze current message at last good split, start a new one.
|
|
86
|
+
const next = this.buffer + text;
|
|
87
|
+
if (next.length > MAX_LEN) {
|
|
88
|
+
const splitAt = findSplit(next, MAX_LEN);
|
|
89
|
+
const head = next.slice(0, splitAt);
|
|
90
|
+
const tail = next.slice(splitAt).trimStart();
|
|
91
|
+
this.buffer = head;
|
|
92
|
+
this.dirty = true;
|
|
93
|
+
await this.flushNow(); // commits head into current message
|
|
94
|
+
this.buffer = '';
|
|
95
|
+
this.currentMessageId = undefined;
|
|
96
|
+
this.firstSendPromise = undefined;
|
|
97
|
+
this.dirty = false;
|
|
98
|
+
if (this.debounceTimer) {
|
|
99
|
+
clearTimeout(this.debounceTimer);
|
|
100
|
+
this.debounceTimer = undefined;
|
|
101
|
+
}
|
|
102
|
+
if (tail)
|
|
103
|
+
await this.append(tail);
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
this.buffer = next;
|
|
107
|
+
this.dirty = true;
|
|
108
|
+
this.scheduleFlush();
|
|
109
|
+
}
|
|
110
|
+
/** Flush any pending edit and close the stream. Idempotent. */
|
|
111
|
+
async end() {
|
|
112
|
+
if (this.ended)
|
|
113
|
+
return;
|
|
114
|
+
this.ended = true;
|
|
115
|
+
if (this.debounceTimer) {
|
|
116
|
+
clearTimeout(this.debounceTimer);
|
|
117
|
+
this.debounceTimer = undefined;
|
|
118
|
+
}
|
|
119
|
+
while (this.inFlight)
|
|
120
|
+
await sleep(50);
|
|
121
|
+
if (this.dirty)
|
|
122
|
+
await this.flushNow();
|
|
123
|
+
}
|
|
124
|
+
scheduleFlush() {
|
|
125
|
+
if (this.debounceTimer)
|
|
126
|
+
return;
|
|
127
|
+
this.debounceTimer = setTimeout(async () => {
|
|
128
|
+
this.debounceTimer = undefined;
|
|
129
|
+
if (this.inFlight) {
|
|
130
|
+
this.scheduleFlush();
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
await this.flushNow();
|
|
134
|
+
if (this.dirty && !this.ended)
|
|
135
|
+
this.scheduleFlush();
|
|
136
|
+
}, this.opts.debounceMs);
|
|
137
|
+
}
|
|
138
|
+
async flushNow() {
|
|
139
|
+
if (!this.dirty)
|
|
140
|
+
return;
|
|
141
|
+
if (this.currentMessageId === undefined) {
|
|
142
|
+
// No active message yet — likely the first send is still in flight
|
|
143
|
+
// or the recovery path cleared it. Caller's next append() will
|
|
144
|
+
// re-create one.
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
this.inFlight = true;
|
|
148
|
+
this.dirty = false;
|
|
149
|
+
const snapshot = this.buffer;
|
|
150
|
+
try {
|
|
151
|
+
const payload = this.opts.markdown ? markdownToFormattable(snapshot) : snapshot;
|
|
152
|
+
await this.bot.api.editMessageText({
|
|
153
|
+
chat_id: this.chatId,
|
|
154
|
+
message_id: this.currentMessageId,
|
|
155
|
+
text: payload,
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
catch (e) {
|
|
159
|
+
const msg = String(e?.message ?? e);
|
|
160
|
+
if (msg.includes('message is not modified')) {
|
|
161
|
+
// identical content — fine
|
|
162
|
+
}
|
|
163
|
+
else if (msg.includes('message to edit not found')) {
|
|
164
|
+
// message gone (deleted by user, etc.) — restart with a fresh send
|
|
165
|
+
this.currentMessageId = undefined;
|
|
166
|
+
this.firstSendPromise = undefined;
|
|
167
|
+
this.dirty = true;
|
|
168
|
+
}
|
|
169
|
+
else {
|
|
170
|
+
this.dirty = true;
|
|
171
|
+
this.opts.onError(e);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
finally {
|
|
175
|
+
this.inFlight = false;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* GramIO plugin. Adds `ctx.startStream(opts?)` on every message context.
|
|
181
|
+
*
|
|
182
|
+
* Defaults set here apply to every stream; per-call options in
|
|
183
|
+
* `ctx.startStream({...})` override them.
|
|
184
|
+
*/
|
|
185
|
+
export const llmStream = (defaults = {}) => new Plugin('@adriangalilea/utils/bot/llm-stream').derive('message', (ctx) => ({
|
|
186
|
+
// gramio's `message` scope guarantees `ctx.chat`. `ctx.bot.api` is on
|
|
187
|
+
// every Context. Structural compat → no cast needed.
|
|
188
|
+
startStream: (opts = {}) => new MarkdownStreamer(ctx, { ...defaults, ...opts }),
|
|
189
|
+
}));
|
|
190
|
+
function findSplit(text, maxLen) {
|
|
191
|
+
// Prefer paragraph break, then line, then space. Reject splits in the
|
|
192
|
+
// first half — better to truncate at maxLen than to leave a stub.
|
|
193
|
+
for (const sep of ['\n\n', '\n', ' ']) {
|
|
194
|
+
const idx = text.lastIndexOf(sep, maxLen);
|
|
195
|
+
if (idx > maxLen / 2)
|
|
196
|
+
return idx;
|
|
197
|
+
}
|
|
198
|
+
return maxLen;
|
|
199
|
+
}
|
|
200
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
201
|
+
//# sourceMappingURL=llm-stream.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"llm-stream.js","sourceRoot":"","sources":["../../src/bot/llm-stream.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AACH,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAA;AAC/B,OAAO,EAAE,qBAAqB,EAAE,MAAM,yBAAyB,CAAA;AAE/D,MAAM,OAAO,GAAG,IAAI,CAAA,CAAC,2DAA2D;AAChF,MAAM,mBAAmB,GAAG,GAAG,CAAA;AAa/B,MAAM,OAAO,gBAAgB;IACnB,MAAM,GAAG,EAAE,CAAA;IACX,gBAAgB,CAAS;IACzB,gBAAgB,CAAgB;IAChC,aAAa,CAAgC;IAC7C,QAAQ,GAAG,KAAK,CAAA;IAChB,KAAK,GAAG,KAAK,CAAA;IACb,KAAK,GAAG,KAAK,CAAA;IAEb,MAAM,CAAQ;IACtB,4EAA4E;IAC5E,0EAA0E;IAC1E,4EAA4E;IACpE,GAAG,CAYV;IACO,IAAI,CAAyB;IAErC,YACE,GAA2D,EAC3D,IAAmB;QAEnB,IAAI,CAAC,MAAM,GAAG,GAAG,CAAC,IAAI,CAAC,EAAE,CAAA;QACzB,IAAI,CAAC,GAAG,GAAG,GAAG,CAAC,GAAG,CAAA;QAClB,IAAI,CAAC,IAAI,GAAG;YACV,UAAU,EAAE,IAAI,CAAC,UAAU,IAAI,mBAAmB;YAClD,WAAW,EAAE,IAAI,CAAC,WAAW,IAAI,GAAG;YACpC,QAAQ,EAAE,IAAI,CAAC,QAAQ,IAAI,IAAI;YAC/B,OAAO,EAAE,IAAI,CAAC,OAAO,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC,cAAc,EAAE,CAAC,CAAC,CAAC;SACnE,CAAA;IACH,CAAC;IAED,kDAAkD;IAClD,KAAK,CAAC,MAAM,CAAC,IAAY;QACvB,IAAI,IAAI,CAAC,KAAK;YAAE,MAAM,IAAI,KAAK,CAAC,sBAAsB,CAAC,CAAA;QACvD,IAAI,CAAC,IAAI;YAAE,OAAM;QAEjB,qEAAqE;QACrE,2EAA2E;QAC3E,IAAI,IAAI,CAAC,gBAAgB,KAAK,SAAS,IAAI,CAAC,IAAI,CAAC,gBAAgB,EAAE,CAAC;YAClE,IAAI,CAAC,gBAAgB,GAAG,CAAC,KAAK,IAAI,EAAE;gBAClC,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,WAAW,CAAC;oBAC1C,OAAO,EAAE,IAAI,CAAC,MAAM;oBACpB,IAAI,EAAE,IAAI,CAAC,IAAI,CAAC,WAAW;iBAC5B,CAAC,CAAA;gBACF,IAAI,CAAC,gBAAgB,GAAG,IAAI,CAAC,UAAU,CAAA;YACzC,CAAC,CAAC,EAAE,CAAA;QACN,CAAC;QACD,IAAI,IAAI,CAAC,gBAAgB;YAAE,MAAM,IAAI,CAAC,gBAAgB,CAAA;QAEtD,wEAAwE;QACxE,MAAM,IAAI,GAAG,IAAI,CAAC,MAAM,GAAG,IAAI,CAAA;QAC/B,IAAI,IAAI,CAAC,MAAM,GAAG,OAAO,EAAE,CAAC;YAC1B,MAAM,OAAO,GAAG,SAAS,CAAC,IAAI,EAAE,OAAO,CAAC,CAAA;YACxC,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,OAAO,CAAC,CAAA;YACnC,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,SAAS,EAAE,CAAA;YAE5C,IAAI,CAAC,MAAM,GAAG,IAAI,CAAA;YAClB,IAAI,CAAC,KAAK,GAAG,IAAI,CAAA;YACjB,MAAM,IAAI,CAAC,QAAQ,EAAE,CAAA,CAAC,oCAAoC;YAE1D,IAAI,CAAC,MAAM,GAAG,EAAE,CAAA;YAChB,IAAI,CAAC,gBAAgB,GAAG,SAAS,CAAA;YACjC,IAAI,CAAC,gBAAgB,GAAG,SAAS,CAAA;YACjC,IAAI,CAAC,KAAK,GAAG,KAAK,CAAA;YAClB,IAAI,IAAI,CAAC,aAAa,EAAE,CAAC;gBACvB,YAAY,CAAC,IAAI,CAAC,aAAa,CAAC,CAAA;gBAChC,IAAI,CAAC,aAAa,GAAG,SAAS,CAAA;YAChC,CAAC;YAED,IAAI,IAAI;gBAAE,MAAM,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,CAAA;YACjC,OAAM;QACR,CAAC;QAED,IAAI,CAAC,MAAM,GAAG,IAAI,CAAA;QAClB,IAAI,CAAC,KAAK,GAAG,IAAI,CAAA;QACjB,IAAI,CAAC,aAAa,EAAE,CAAA;IACtB,CAAC;IAED,+DAA+D;IAC/D,KAAK,CAAC,GAAG;QACP,IAAI,IAAI,CAAC,KAAK;YAAE,OAAM;QACtB,IAAI,CAAC,KAAK,GAAG,IAAI,CAAA;QACjB,IAAI,IAAI,CAAC,aAAa,EAAE,CAAC;YACvB,YAAY,CAAC,IAAI,CAAC,aAAa,CAAC,CAAA;YAChC,IAAI,CAAC,aAAa,GAAG,SAAS,CAAA;QAChC,CAAC;QACD,OAAO,IAAI,CAAC,QAAQ;YAAE,MAAM,KAAK,CAAC,EAAE,CAAC,CAAA;QACrC,IAAI,IAAI,CAAC,KAAK;YAAE,MAAM,IAAI,CAAC,QAAQ,EAAE,CAAA;IACvC,CAAC;IAEO,aAAa;QACnB,IAAI,IAAI,CAAC,aAAa;YAAE,OAAM;QAC9B,IAAI,CAAC,aAAa,GAAG,UAAU,CAAC,KAAK,IAAI,EAAE;YACzC,IAAI,CAAC,aAAa,GAAG,SAAS,CAAA;YAC9B,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC;gBAClB,IAAI,CAAC,aAAa,EAAE,CAAA;gBACpB,OAAM;YACR,CAAC;YACD,MAAM,IAAI,CAAC,QAAQ,EAAE,CAAA;YACrB,IAAI,IAAI,CAAC,KAAK,IAAI,CAAC,IAAI,CAAC,KAAK;gBAAE,IAAI,CAAC,aAAa,EAAE,CAAA;QACrD,CAAC,EAAE,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,CAAA;IAC1B,CAAC;IAEO,KAAK,CAAC,QAAQ;QACpB,IAAI,CAAC,IAAI,CAAC,KAAK;YAAE,OAAM;QACvB,IAAI,IAAI,CAAC,gBAAgB,KAAK,SAAS,EAAE,CAAC;YACxC,mEAAmE;YACnE,+DAA+D;YAC/D,iBAAiB;YACjB,OAAM;QACR,CAAC;QACD,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAA;QACpB,IAAI,CAAC,KAAK,GAAG,KAAK,CAAA;QAClB,MAAM,QAAQ,GAAG,IAAI,CAAC,MAAM,CAAA;QAC5B,IAAI,CAAC;YACH,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,qBAAqB,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAA;YAC/E,MAAM,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,eAAe,CAAC;gBACjC,OAAO,EAAE,IAAI,CAAC,MAAM;gBACpB,UAAU,EAAE,IAAI,CAAC,gBAAgB;gBACjC,IAAI,EAAE,OAAO;aACd,CAAC,CAAA;QACJ,CAAC;QAAC,OAAO,CAAC,EAAE,CAAC;YACX,MAAM,GAAG,GAAG,MAAM,CAAE,CAAsC,EAAE,OAAO,IAAI,CAAC,CAAC,CAAA;YACzE,IAAI,GAAG,CAAC,QAAQ,CAAC,yBAAyB,CAAC,EAAE,CAAC;gBAC5C,2BAA2B;YAC7B,CAAC;iBAAM,IAAI,GAAG,CAAC,QAAQ,CAAC,2BAA2B,CAAC,EAAE,CAAC;gBACrD,mEAAmE;gBACnE,IAAI,CAAC,gBAAgB,GAAG,SAAS,CAAA;gBACjC,IAAI,CAAC,gBAAgB,GAAG,SAAS,CAAA;gBACjC,IAAI,CAAC,KAAK,GAAG,IAAI,CAAA;YACnB,CAAC;iBAAM,CAAC;gBACN,IAAI,CAAC,KAAK,GAAG,IAAI,CAAA;gBACjB,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,CAAA;YACtB,CAAC;QACH,CAAC;gBAAS,CAAC;YACT,IAAI,CAAC,QAAQ,GAAG,KAAK,CAAA;QACvB,CAAC;IACH,CAAC;CACF;AAED;;;;;GAKG;AACH,MAAM,CAAC,MAAM,SAAS,GAAG,CAAC,WAA0B,EAAE,EAAE,EAAE,CACxD,IAAI,MAAM,CAAC,qCAAqC,CAAC,CAAC,MAAM,CAAC,SAAS,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC;IAC5E,sEAAsE;IACtE,qDAAqD;IACrD,WAAW,EAAE,CAAC,OAAsB,EAAE,EAAE,EAAE,CACxC,IAAI,gBAAgB,CAAC,GAAG,EAAE,EAAE,GAAG,QAAQ,EAAE,GAAG,IAAI,EAAE,CAAC;CACtD,CAAC,CAAC,CAAA;AAEL,SAAS,SAAS,CAAC,IAAY,EAAE,MAAc;IAC7C,sEAAsE;IACtE,kEAAkE;IAClE,KAAK,MAAM,GAAG,IAAI,CAAC,MAAM,EAAE,IAAI,EAAE,GAAG,CAAC,EAAE,CAAC;QACtC,MAAM,GAAG,GAAG,IAAI,CAAC,WAAW,CAAC,GAAG,EAAE,MAAM,CAAC,CAAA;QACzC,IAAI,GAAG,GAAG,MAAM,GAAG,CAAC;YAAE,OAAO,GAAG,CAAA;IAClC,CAAC;IACD,OAAO,MAAM,CAAA;AACf,CAAC;AAED,MAAM,KAAK,GAAG,CAAC,EAAU,EAAE,EAAE,CAAC,IAAI,OAAO,CAAO,CAAC,CAAC,EAAE,EAAE,CAAC,UAAU,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAA"}
|
package/dist/offensive.d.ts
CHANGED
|
@@ -1,29 +1,31 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Offensive programming primitives + typed boundary errors.
|
|
3
3
|
*
|
|
4
|
-
* "A confused program SHOULD scream"
|
|
4
|
+
* "A confused program SHOULD scream." — John Carmack
|
|
5
5
|
*
|
|
6
6
|
* Throw, always. An uncaught Panic crashes the process with a full stack trace.
|
|
7
7
|
* Zero dependencies. Works identically in Node, Deno, Bun, and browsers.
|
|
8
8
|
*
|
|
9
|
-
*
|
|
10
|
-
* assert(cond, ...msg)
|
|
11
|
-
* panic(...msg)
|
|
12
|
-
*
|
|
13
|
-
*
|
|
9
|
+
* Fail-fast primitives (throw Panic — bugs in us):
|
|
10
|
+
* assert(cond, ...msg) invariant checking, narrows types via `asserts`
|
|
11
|
+
* panic(...msg) impossible state reached
|
|
12
|
+
* assertNever(value, ...msg) exhaustiveness check — compile error on missed cases
|
|
13
|
+
* must(() => expr) unwrap-or-die for operations (sync + async)
|
|
14
|
+
* unwrap(value, ...msg) unwrap nullable T | null | undefined → T
|
|
14
15
|
*
|
|
15
|
-
*
|
|
16
|
+
* Typed boundary errors (throw SourcedError — the external system failed):
|
|
17
|
+
* SourcedError typed error with source/operation/status/context
|
|
18
|
+
* isSourcedError(e, source?) type guard for catch-site narrowing
|
|
16
19
|
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
* const data = readFileSync(path, 'utf-8')
|
|
20
|
-
* return data
|
|
21
|
-
* } catch (err) {
|
|
22
|
-
* check(err)
|
|
23
|
-
* }
|
|
20
|
+
* Panics are bugs. SourcedErrors are boundary failures. Keep them separate at
|
|
21
|
+
* catch boundaries:
|
|
24
22
|
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
23
|
+
* try { await doWork() }
|
|
24
|
+
* catch (e) {
|
|
25
|
+
* if (e instanceof Panic) throw e // bug in us — crash
|
|
26
|
+
* if (isSourcedError(e, 'stripe')) { ... } // stripe failed — handle
|
|
27
|
+
* throw e // unknown — re-throw
|
|
28
|
+
* }
|
|
27
29
|
*/
|
|
28
30
|
/**
|
|
29
31
|
* Distinct error class for offensive programming failures.
|
|
@@ -61,6 +63,23 @@ export declare function assert(condition: boolean, ...msg: any[]): asserts condi
|
|
|
61
63
|
* }
|
|
62
64
|
*/
|
|
63
65
|
export declare function panic(...msg: any[]): never;
|
|
66
|
+
/**
|
|
67
|
+
* Exhaustiveness check — compile error if a switch/if misses a case.
|
|
68
|
+
* The `never` type means TS won't let you call this if all cases are handled.
|
|
69
|
+
* Add a new variant to a union → every assertNever site lights up at compile time.
|
|
70
|
+
*
|
|
71
|
+
* @example
|
|
72
|
+
* type Event = { kind: 'click' } | { kind: 'hover' } | { kind: 'scroll' }
|
|
73
|
+
* function handle(e: Event) {
|
|
74
|
+
* switch (e.kind) {
|
|
75
|
+
* case 'click': return handleClick()
|
|
76
|
+
* case 'hover': return handleHover()
|
|
77
|
+
* // forgot 'scroll' → TS error: Argument of type '{ kind: "scroll" }' not assignable to 'never'
|
|
78
|
+
* default: assertNever(e)
|
|
79
|
+
* }
|
|
80
|
+
* }
|
|
81
|
+
*/
|
|
82
|
+
export declare function assertNever(value: never, ...msg: any[]): never;
|
|
64
83
|
/**
|
|
65
84
|
* Must unwraps an operation that should never fail. Handles sync and async.
|
|
66
85
|
*
|
|
@@ -85,11 +104,64 @@ export declare function must<T>(fn: () => T): T;
|
|
|
85
104
|
* const el = unwrap(document.getElementById('app'))
|
|
86
105
|
*/
|
|
87
106
|
export declare function unwrap<T>(value: T | null | undefined, ...msg: any[]): T;
|
|
107
|
+
/**
|
|
108
|
+
* Typed error from a named external source with structured context.
|
|
109
|
+
*
|
|
110
|
+
* Raise at boundaries with the messy world (HTTP APIs, databases, external
|
|
111
|
+
* processes). Every SourcedError carries enough context to reconstruct the
|
|
112
|
+
* failure without a debugger.
|
|
113
|
+
*
|
|
114
|
+
* @example
|
|
115
|
+
* try {
|
|
116
|
+
* return await stripe.charges.create({ customer, amount })
|
|
117
|
+
* } catch (e) {
|
|
118
|
+
* throw new SourcedError({
|
|
119
|
+
* source: 'stripe',
|
|
120
|
+
* operation: 'charge_customer',
|
|
121
|
+
* message: e instanceof Error ? e.message : String(e),
|
|
122
|
+
* status: (e as any)?.statusCode,
|
|
123
|
+
* cause: e,
|
|
124
|
+
* context: { customer, amount },
|
|
125
|
+
* })
|
|
126
|
+
* }
|
|
127
|
+
*/
|
|
128
|
+
export declare class SourcedError<S extends string = string> extends Error {
|
|
129
|
+
readonly source: S;
|
|
130
|
+
readonly operation: string;
|
|
131
|
+
readonly status?: number;
|
|
132
|
+
readonly context: Record<string, unknown>;
|
|
133
|
+
constructor(args: {
|
|
134
|
+
source: S;
|
|
135
|
+
operation: string;
|
|
136
|
+
message: string;
|
|
137
|
+
status?: number;
|
|
138
|
+
cause?: unknown;
|
|
139
|
+
context?: Record<string, unknown>;
|
|
140
|
+
});
|
|
141
|
+
toJSON(): Record<string, unknown>;
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Type guard for SourcedError. Optionally narrows to a specific source.
|
|
145
|
+
*
|
|
146
|
+
* @example
|
|
147
|
+
* try { await charge(customer, amount) }
|
|
148
|
+
* catch (e) {
|
|
149
|
+
* if (isSourcedError(e, 'stripe') && e.status === 402) {
|
|
150
|
+
* // TS knows e.source === 'stripe' here
|
|
151
|
+
* return 'card declined'
|
|
152
|
+
* }
|
|
153
|
+
* throw e
|
|
154
|
+
* }
|
|
155
|
+
*/
|
|
156
|
+
export declare function isSourcedError<S extends string>(e: unknown, source?: S): e is SourcedError<S>;
|
|
88
157
|
export declare const offensive: {
|
|
89
158
|
Panic: typeof Panic;
|
|
90
159
|
assert: typeof assert;
|
|
91
160
|
panic: typeof panic;
|
|
161
|
+
assertNever: typeof assertNever;
|
|
92
162
|
must: typeof must;
|
|
93
163
|
unwrap: typeof unwrap;
|
|
164
|
+
SourcedError: typeof SourcedError;
|
|
165
|
+
isSourcedError: typeof isSourcedError;
|
|
94
166
|
};
|
|
95
167
|
//# sourceMappingURL=offensive.d.ts.map
|
package/dist/offensive.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"offensive.d.ts","sourceRoot":"","sources":["../src/offensive.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"offensive.d.ts","sourceRoot":"","sources":["../src/offensive.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAEH;;;;;;;;;;;;;GAaG;AACH,qBAAa,KAAM,SAAQ,KAAK;gBAClB,OAAO,EAAE,MAAM;CAI5B;AAED;;;;;;GAMG;AACH,wBAAgB,MAAM,CAAC,SAAS,EAAE,OAAO,EAAE,GAAG,GAAG,EAAE,GAAG,EAAE,GAAG,OAAO,CAAC,SAAS,CAE3E;AAED;;;;;;;;;GASG;AACH,wBAAgB,KAAK,CAAC,GAAG,GAAG,EAAE,GAAG,EAAE,GAAG,KAAK,CAE1C;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,KAAK,EAAE,GAAG,GAAG,EAAE,GAAG,EAAE,GAAG,KAAK,CAE9D;AAED;;;;;;;GAOG;AACH,wBAAgB,IAAI,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAA;AACzD,wBAAgB,IAAI,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,CAAC,GAAG,CAAC,CAAA;AAevC;;;;;;;;;;;;GAYG;AACH,wBAAgB,MAAM,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,GAAG,IAAI,GAAG,SAAS,EAAE,GAAG,GAAG,EAAE,GAAG,EAAE,GAAG,CAAC,CAGvE;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,qBAAa,YAAY,CAAC,CAAC,SAAS,MAAM,GAAG,MAAM,CAAE,SAAQ,KAAK;IAChE,QAAQ,CAAC,MAAM,EAAE,CAAC,CAAA;IAClB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;IAC1B,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;gBAE7B,IAAI,EAAE;QAChB,MAAM,EAAE,CAAC,CAAA;QACT,SAAS,EAAE,MAAM,CAAA;QACjB,OAAO,EAAE,MAAM,CAAA;QACf,MAAM,CAAC,EAAE,MAAM,CAAA;QACf,KAAK,CAAC,EAAE,OAAO,CAAA;QACf,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;KAClC;IAUD,MAAM,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;CAUlC;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,cAAc,CAAC,CAAC,SAAS,MAAM,EAAE,CAAC,EAAE,OAAO,EAAE,MAAM,CAAC,EAAE,CAAC,GAAG,CAAC,IAAI,YAAY,CAAC,CAAC,CAAC,CAE7F;AAED,eAAO,MAAM,SAAS;;;;;;;;;CASrB,CAAA"}
|
package/dist/offensive.js
CHANGED
|
@@ -1,29 +1,31 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Offensive programming primitives + typed boundary errors.
|
|
3
3
|
*
|
|
4
|
-
* "A confused program SHOULD scream"
|
|
4
|
+
* "A confused program SHOULD scream." — John Carmack
|
|
5
5
|
*
|
|
6
6
|
* Throw, always. An uncaught Panic crashes the process with a full stack trace.
|
|
7
7
|
* Zero dependencies. Works identically in Node, Deno, Bun, and browsers.
|
|
8
8
|
*
|
|
9
|
-
*
|
|
10
|
-
* assert(cond, ...msg)
|
|
11
|
-
* panic(...msg)
|
|
12
|
-
*
|
|
13
|
-
*
|
|
9
|
+
* Fail-fast primitives (throw Panic — bugs in us):
|
|
10
|
+
* assert(cond, ...msg) invariant checking, narrows types via `asserts`
|
|
11
|
+
* panic(...msg) impossible state reached
|
|
12
|
+
* assertNever(value, ...msg) exhaustiveness check — compile error on missed cases
|
|
13
|
+
* must(() => expr) unwrap-or-die for operations (sync + async)
|
|
14
|
+
* unwrap(value, ...msg) unwrap nullable T | null | undefined → T
|
|
14
15
|
*
|
|
15
|
-
*
|
|
16
|
+
* Typed boundary errors (throw SourcedError — the external system failed):
|
|
17
|
+
* SourcedError typed error with source/operation/status/context
|
|
18
|
+
* isSourcedError(e, source?) type guard for catch-site narrowing
|
|
16
19
|
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
* const data = readFileSync(path, 'utf-8')
|
|
20
|
-
* return data
|
|
21
|
-
* } catch (err) {
|
|
22
|
-
* check(err)
|
|
23
|
-
* }
|
|
20
|
+
* Panics are bugs. SourcedErrors are boundary failures. Keep them separate at
|
|
21
|
+
* catch boundaries:
|
|
24
22
|
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
23
|
+
* try { await doWork() }
|
|
24
|
+
* catch (e) {
|
|
25
|
+
* if (e instanceof Panic) throw e // bug in us — crash
|
|
26
|
+
* if (isSourcedError(e, 'stripe')) { ... } // stripe failed — handle
|
|
27
|
+
* throw e // unknown — re-throw
|
|
28
|
+
* }
|
|
27
29
|
*/
|
|
28
30
|
/**
|
|
29
31
|
* Distinct error class for offensive programming failures.
|
|
@@ -69,6 +71,25 @@ export function assert(condition, ...msg) {
|
|
|
69
71
|
export function panic(...msg) {
|
|
70
72
|
throw new Panic(msg.join(' ') || 'panic');
|
|
71
73
|
}
|
|
74
|
+
/**
|
|
75
|
+
* Exhaustiveness check — compile error if a switch/if misses a case.
|
|
76
|
+
* The `never` type means TS won't let you call this if all cases are handled.
|
|
77
|
+
* Add a new variant to a union → every assertNever site lights up at compile time.
|
|
78
|
+
*
|
|
79
|
+
* @example
|
|
80
|
+
* type Event = { kind: 'click' } | { kind: 'hover' } | { kind: 'scroll' }
|
|
81
|
+
* function handle(e: Event) {
|
|
82
|
+
* switch (e.kind) {
|
|
83
|
+
* case 'click': return handleClick()
|
|
84
|
+
* case 'hover': return handleHover()
|
|
85
|
+
* // forgot 'scroll' → TS error: Argument of type '{ kind: "scroll" }' not assignable to 'never'
|
|
86
|
+
* default: assertNever(e)
|
|
87
|
+
* }
|
|
88
|
+
* }
|
|
89
|
+
*/
|
|
90
|
+
export function assertNever(value, ...msg) {
|
|
91
|
+
throw new Panic(msg.join(' ') || `assertNever: unexpected value: ${JSON.stringify(value)}`);
|
|
92
|
+
}
|
|
72
93
|
export function must(fn) {
|
|
73
94
|
try {
|
|
74
95
|
const result = fn();
|
|
@@ -101,11 +122,76 @@ export function unwrap(value, ...msg) {
|
|
|
101
122
|
throw new Panic(msg.join(' ') || `unwrap: got ${value}`);
|
|
102
123
|
return value;
|
|
103
124
|
}
|
|
125
|
+
/**
|
|
126
|
+
* Typed error from a named external source with structured context.
|
|
127
|
+
*
|
|
128
|
+
* Raise at boundaries with the messy world (HTTP APIs, databases, external
|
|
129
|
+
* processes). Every SourcedError carries enough context to reconstruct the
|
|
130
|
+
* failure without a debugger.
|
|
131
|
+
*
|
|
132
|
+
* @example
|
|
133
|
+
* try {
|
|
134
|
+
* return await stripe.charges.create({ customer, amount })
|
|
135
|
+
* } catch (e) {
|
|
136
|
+
* throw new SourcedError({
|
|
137
|
+
* source: 'stripe',
|
|
138
|
+
* operation: 'charge_customer',
|
|
139
|
+
* message: e instanceof Error ? e.message : String(e),
|
|
140
|
+
* status: (e as any)?.statusCode,
|
|
141
|
+
* cause: e,
|
|
142
|
+
* context: { customer, amount },
|
|
143
|
+
* })
|
|
144
|
+
* }
|
|
145
|
+
*/
|
|
146
|
+
export class SourcedError extends Error {
|
|
147
|
+
source;
|
|
148
|
+
operation;
|
|
149
|
+
status;
|
|
150
|
+
context;
|
|
151
|
+
constructor(args) {
|
|
152
|
+
const status = args.status != null ? ` status=${args.status}` : '';
|
|
153
|
+
super(`[${args.source}:${args.operation}${status}] ${args.message}`, { cause: args.cause });
|
|
154
|
+
this.name = 'SourcedError';
|
|
155
|
+
this.source = args.source;
|
|
156
|
+
this.operation = args.operation;
|
|
157
|
+
this.status = args.status;
|
|
158
|
+
this.context = args.context ?? {};
|
|
159
|
+
}
|
|
160
|
+
toJSON() {
|
|
161
|
+
return {
|
|
162
|
+
source: this.source,
|
|
163
|
+
operation: this.operation,
|
|
164
|
+
status: this.status,
|
|
165
|
+
message: this.message,
|
|
166
|
+
context: this.context,
|
|
167
|
+
cause: this.cause instanceof Error ? this.cause.message : this.cause != null ? String(this.cause) : null,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Type guard for SourcedError. Optionally narrows to a specific source.
|
|
173
|
+
*
|
|
174
|
+
* @example
|
|
175
|
+
* try { await charge(customer, amount) }
|
|
176
|
+
* catch (e) {
|
|
177
|
+
* if (isSourcedError(e, 'stripe') && e.status === 402) {
|
|
178
|
+
* // TS knows e.source === 'stripe' here
|
|
179
|
+
* return 'card declined'
|
|
180
|
+
* }
|
|
181
|
+
* throw e
|
|
182
|
+
* }
|
|
183
|
+
*/
|
|
184
|
+
export function isSourcedError(e, source) {
|
|
185
|
+
return e instanceof SourcedError && (source === undefined || e.source === source);
|
|
186
|
+
}
|
|
104
187
|
export const offensive = {
|
|
105
188
|
Panic,
|
|
106
189
|
assert,
|
|
107
190
|
panic,
|
|
191
|
+
assertNever,
|
|
108
192
|
must,
|
|
109
193
|
unwrap,
|
|
194
|
+
SourcedError,
|
|
195
|
+
isSourcedError,
|
|
110
196
|
};
|
|
111
197
|
//# sourceMappingURL=offensive.js.map
|
package/dist/offensive.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"offensive.js","sourceRoot":"","sources":["../src/offensive.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"offensive.js","sourceRoot":"","sources":["../src/offensive.ts"],"names":[],"mappings":"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"}
|