instar 0.4.10 → 0.5.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/dist/cli.js +1 -0
- package/dist/commands/server.d.ts +3 -0
- package/dist/commands/server.js +20 -3
- package/dist/core/types.d.ts +12 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.js +2 -0
- package/dist/lifeline/ServerSupervisor.js +2 -1
- package/dist/publishing/TelegraphService.d.ts +124 -0
- package/dist/publishing/TelegraphService.js +386 -0
- package/dist/scaffold/templates.js +35 -0
- package/dist/server/AgentServer.d.ts +2 -0
- package/dist/server/AgentServer.js +1 -0
- package/dist/server/routes.d.ts +2 -0
- package/dist/server/routes.js +69 -0
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -353,6 +353,7 @@ serverCmd
|
|
|
353
353
|
.command('start')
|
|
354
354
|
.description('Start the agent server')
|
|
355
355
|
.option('--foreground', 'Run in foreground (default: background via tmux)')
|
|
356
|
+
.option('--no-telegram', 'Skip Telegram polling (use when lifeline manages Telegram)')
|
|
356
357
|
.option('-d, --dir <path>', 'Project directory')
|
|
357
358
|
.action(startServer);
|
|
358
359
|
serverCmd
|
|
@@ -10,6 +10,9 @@
|
|
|
10
10
|
interface StartOptions {
|
|
11
11
|
foreground?: boolean;
|
|
12
12
|
dir?: string;
|
|
13
|
+
/** When false, skip Telegram polling (used when lifeline owns the Telegram connection).
|
|
14
|
+
* Commander maps --no-telegram to telegram: false. */
|
|
15
|
+
telegram?: boolean;
|
|
13
16
|
}
|
|
14
17
|
export declare function startServer(options: StartOptions): Promise<void>;
|
|
15
18
|
export declare function stopServer(options: {
|
package/dist/commands/server.js
CHANGED
|
@@ -22,6 +22,7 @@ import { FeedbackManager } from '../core/FeedbackManager.js';
|
|
|
22
22
|
import { DispatchManager } from '../core/DispatchManager.js';
|
|
23
23
|
import { UpdateChecker } from '../core/UpdateChecker.js';
|
|
24
24
|
import { registerPort, unregisterPort, startHeartbeat } from '../core/PortRegistry.js';
|
|
25
|
+
import { TelegraphService } from '../publishing/TelegraphService.js';
|
|
25
26
|
/**
|
|
26
27
|
* Respawn a session for a topic, including thread history in the bootstrap.
|
|
27
28
|
* This prevents "thread drift" where respawned sessions lose context.
|
|
@@ -318,10 +319,14 @@ export async function startServer(options) {
|
|
|
318
319
|
scheduler.start();
|
|
319
320
|
console.log(pc.green(' Scheduler started'));
|
|
320
321
|
}
|
|
321
|
-
// Set up Telegram if configured
|
|
322
|
+
// Set up Telegram if configured (skip if lifeline owns the connection)
|
|
322
323
|
let telegram;
|
|
323
324
|
const telegramConfig = config.messaging.find(m => m.type === 'telegram' && m.enabled);
|
|
324
|
-
|
|
325
|
+
const skipTelegram = options.telegram === false; // --no-telegram sets telegram: false
|
|
326
|
+
if (skipTelegram && telegramConfig) {
|
|
327
|
+
console.log(pc.dim(' Telegram polling skipped (--no-telegram flag)'));
|
|
328
|
+
}
|
|
329
|
+
if (telegramConfig && !skipTelegram) {
|
|
325
330
|
telegram = new TelegramAdapter(telegramConfig.config, config.stateDir);
|
|
326
331
|
await telegram.start();
|
|
327
332
|
console.log(pc.green(' Telegram connected'));
|
|
@@ -380,7 +385,19 @@ export async function startServer(options) {
|
|
|
380
385
|
console.log(pc.green(` Instar ${info.currentVersion} is up to date`));
|
|
381
386
|
}
|
|
382
387
|
}).catch(() => { });
|
|
383
|
-
|
|
388
|
+
// Set up Telegraph publishing (auto-enabled when config exists or Telegram is configured)
|
|
389
|
+
let publisher;
|
|
390
|
+
const pubConfig = config.publishing;
|
|
391
|
+
if (pubConfig?.enabled !== false) {
|
|
392
|
+
publisher = new TelegraphService({
|
|
393
|
+
stateDir: config.stateDir,
|
|
394
|
+
shortName: pubConfig?.shortName || config.projectName,
|
|
395
|
+
authorName: pubConfig?.authorName,
|
|
396
|
+
authorUrl: pubConfig?.authorUrl,
|
|
397
|
+
});
|
|
398
|
+
console.log(pc.green(` Publishing enabled (Telegraph)`));
|
|
399
|
+
}
|
|
400
|
+
const server = new AgentServer({ config, sessionManager, state, scheduler, telegram, relationships, feedback, dispatches, updateChecker, publisher });
|
|
384
401
|
await server.start();
|
|
385
402
|
// Graceful shutdown
|
|
386
403
|
const shutdown = async () => {
|
package/dist/core/types.d.ts
CHANGED
|
@@ -334,11 +334,23 @@ export interface InstarConfig {
|
|
|
334
334
|
dispatches?: DispatchConfig;
|
|
335
335
|
/** Update configuration */
|
|
336
336
|
updates?: UpdateConfig;
|
|
337
|
+
/** Publishing (Telegraph) config */
|
|
338
|
+
publishing?: PublishingConfig;
|
|
337
339
|
/** Request timeout in milliseconds (default: 30000) */
|
|
338
340
|
requestTimeoutMs?: number;
|
|
339
341
|
/** Instar version (from package.json) */
|
|
340
342
|
version?: string;
|
|
341
343
|
}
|
|
344
|
+
export interface PublishingConfig {
|
|
345
|
+
/** Whether publishing is enabled (default: true when Telegram is configured) */
|
|
346
|
+
enabled: boolean;
|
|
347
|
+
/** Short name for the Telegraph account */
|
|
348
|
+
shortName?: string;
|
|
349
|
+
/** Author name shown on published pages */
|
|
350
|
+
authorName?: string;
|
|
351
|
+
/** Author URL shown on published pages */
|
|
352
|
+
authorUrl?: string;
|
|
353
|
+
}
|
|
342
354
|
export interface DispatchConfig {
|
|
343
355
|
/** Whether dispatch polling is enabled */
|
|
344
356
|
enabled: boolean;
|
package/dist/index.d.ts
CHANGED
|
@@ -25,6 +25,8 @@ export { QuotaTracker } from './monitoring/QuotaTracker.js';
|
|
|
25
25
|
export { SleepWakeDetector } from './core/SleepWakeDetector.js';
|
|
26
26
|
export { TelegramAdapter } from './messaging/TelegramAdapter.js';
|
|
27
27
|
export type { TelegramConfig } from './messaging/TelegramAdapter.js';
|
|
28
|
-
export
|
|
28
|
+
export { TelegraphService, markdownToNodes, parseInline } from './publishing/TelegraphService.js';
|
|
29
|
+
export type { TelegraphConfig, TelegraphNode, TelegraphElement, TelegraphPage, PublishedPage } from './publishing/TelegraphService.js';
|
|
30
|
+
export type { Session, SessionStatus, SessionManagerConfig, ModelTier, JobDefinition, JobPriority, JobExecution, JobState, JobSchedulerConfig, UserProfile, UserChannel, UserPreferences, Message, OutgoingMessage, MessagingAdapter, MessagingAdapterConfig, QuotaState, AccountQuota, HealthStatus, ComponentHealth, ActivityEvent, InstarConfig, MonitoringConfig, RelationshipRecord, RelationshipManagerConfig, InteractionSummary, FeedbackItem, FeedbackConfig, UpdateInfo, UpdateResult, DispatchConfig, UpdateConfig, PublishingConfig, } from './core/types.js';
|
|
29
31
|
export type { Dispatch, DispatchCheckResult, DispatchEvaluation, EvaluationDecision, DispatchFeedback, DispatchStats } from './core/DispatchManager.js';
|
|
30
32
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.js
CHANGED
|
@@ -27,4 +27,6 @@ export { QuotaTracker } from './monitoring/QuotaTracker.js';
|
|
|
27
27
|
export { SleepWakeDetector } from './core/SleepWakeDetector.js';
|
|
28
28
|
// Messaging
|
|
29
29
|
export { TelegramAdapter } from './messaging/TelegramAdapter.js';
|
|
30
|
+
// Publishing
|
|
31
|
+
export { TelegraphService, markdownToNodes, parseInline } from './publishing/TelegraphService.js';
|
|
30
32
|
//# sourceMappingURL=index.js.map
|
|
@@ -96,7 +96,8 @@ export class ServerSupervisor extends EventEmitter {
|
|
|
96
96
|
try {
|
|
97
97
|
// Get the instar CLI path
|
|
98
98
|
const cliPath = new URL('../../cli.js', import.meta.url).pathname;
|
|
99
|
-
|
|
99
|
+
// --no-telegram: lifeline owns the Telegram connection, server should not poll
|
|
100
|
+
const nodeCmd = ['node', cliPath, 'server', 'start', '--foreground', '--no-telegram']
|
|
100
101
|
.map(arg => `'${arg.replace(/'/g, "'\\''")}'`)
|
|
101
102
|
.join(' ');
|
|
102
103
|
execFileSync(this.tmuxPath, [
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Telegraph publishing service for Instar agents.
|
|
3
|
+
*
|
|
4
|
+
* Converts markdown to Telegraph Node format and publishes
|
|
5
|
+
* content via the Telegraph API (telegra.ph). Zero-config,
|
|
6
|
+
* no rate limits, instant web pages accessible from anywhere.
|
|
7
|
+
*
|
|
8
|
+
* Telegraph API docs: https://telegra.ph/api
|
|
9
|
+
*/
|
|
10
|
+
/** Valid Telegraph element tags */
|
|
11
|
+
export type TelegraphTag = 'a' | 'aside' | 'b' | 'blockquote' | 'br' | 'code' | 'em' | 'figcaption' | 'figure' | 'h3' | 'h4' | 'hr' | 'i' | 'iframe' | 'img' | 'li' | 'ol' | 'p' | 'pre' | 's' | 'strong' | 'u' | 'ul' | 'video';
|
|
12
|
+
/** Telegraph element node — tag with optional attrs and children */
|
|
13
|
+
export interface TelegraphElement {
|
|
14
|
+
tag: TelegraphTag;
|
|
15
|
+
attrs?: {
|
|
16
|
+
href?: string;
|
|
17
|
+
src?: string;
|
|
18
|
+
};
|
|
19
|
+
children?: TelegraphNode[];
|
|
20
|
+
}
|
|
21
|
+
/** A Telegraph node is either a text string or an element */
|
|
22
|
+
export type TelegraphNode = string | TelegraphElement;
|
|
23
|
+
export interface TelegraphAccount {
|
|
24
|
+
short_name: string;
|
|
25
|
+
author_name?: string;
|
|
26
|
+
author_url?: string;
|
|
27
|
+
access_token: string;
|
|
28
|
+
auth_url?: string;
|
|
29
|
+
}
|
|
30
|
+
export interface TelegraphPage {
|
|
31
|
+
path: string;
|
|
32
|
+
url: string;
|
|
33
|
+
title: string;
|
|
34
|
+
description?: string;
|
|
35
|
+
views?: number;
|
|
36
|
+
can_edit?: boolean;
|
|
37
|
+
}
|
|
38
|
+
export interface TelegraphPageList {
|
|
39
|
+
total_count: number;
|
|
40
|
+
pages: TelegraphPage[];
|
|
41
|
+
}
|
|
42
|
+
export interface PublishedPage {
|
|
43
|
+
path: string;
|
|
44
|
+
url: string;
|
|
45
|
+
title: string;
|
|
46
|
+
publishedAt: string;
|
|
47
|
+
updatedAt?: string;
|
|
48
|
+
/** Original markdown for diffing/re-publishing */
|
|
49
|
+
markdownHash?: string;
|
|
50
|
+
}
|
|
51
|
+
export interface PublishingState {
|
|
52
|
+
accessToken?: string;
|
|
53
|
+
shortName?: string;
|
|
54
|
+
authorName?: string;
|
|
55
|
+
pages: PublishedPage[];
|
|
56
|
+
}
|
|
57
|
+
export interface TelegraphConfig {
|
|
58
|
+
/** State directory where publishing.json is stored */
|
|
59
|
+
stateDir: string;
|
|
60
|
+
/** Short name for the Telegraph account (e.g., agent name) */
|
|
61
|
+
shortName?: string;
|
|
62
|
+
/** Author name shown on published pages */
|
|
63
|
+
authorName?: string;
|
|
64
|
+
/** Author URL shown on published pages */
|
|
65
|
+
authorUrl?: string;
|
|
66
|
+
}
|
|
67
|
+
export declare class TelegraphService {
|
|
68
|
+
private config;
|
|
69
|
+
private stateFile;
|
|
70
|
+
private state;
|
|
71
|
+
constructor(config: TelegraphConfig);
|
|
72
|
+
/**
|
|
73
|
+
* Ensure a Telegraph account exists. Creates one if needed.
|
|
74
|
+
* Returns the access token.
|
|
75
|
+
*/
|
|
76
|
+
ensureAccount(): Promise<string>;
|
|
77
|
+
/**
|
|
78
|
+
* Create a new Telegraph account.
|
|
79
|
+
*/
|
|
80
|
+
createAccount(shortName: string, authorName?: string): Promise<TelegraphAccount>;
|
|
81
|
+
/**
|
|
82
|
+
* Publish markdown content as a Telegraph page.
|
|
83
|
+
* Returns the page URL and path.
|
|
84
|
+
*/
|
|
85
|
+
publishPage(title: string, markdown: string): Promise<TelegraphPage>;
|
|
86
|
+
/**
|
|
87
|
+
* Edit an existing Telegraph page.
|
|
88
|
+
*/
|
|
89
|
+
editPage(pagePath: string, title: string, markdown: string): Promise<TelegraphPage>;
|
|
90
|
+
/**
|
|
91
|
+
* Get page view count from Telegraph.
|
|
92
|
+
*/
|
|
93
|
+
getPageViews(pagePath: string): Promise<number>;
|
|
94
|
+
/**
|
|
95
|
+
* List all locally tracked published pages.
|
|
96
|
+
*/
|
|
97
|
+
listPages(): PublishedPage[];
|
|
98
|
+
/**
|
|
99
|
+
* Get state for inspection/testing.
|
|
100
|
+
*/
|
|
101
|
+
getState(): PublishingState;
|
|
102
|
+
private loadState;
|
|
103
|
+
private saveState;
|
|
104
|
+
private apiCall;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Convert markdown text to Telegraph Node[] format.
|
|
108
|
+
*
|
|
109
|
+
* Supports: headings (h3/h4), bold, italic, strikethrough, code,
|
|
110
|
+
* code blocks, links, images, blockquotes, lists (ol/ul),
|
|
111
|
+
* horizontal rules, and paragraphs.
|
|
112
|
+
*
|
|
113
|
+
* Telegraph only supports h3 and h4, so # and ## map to h3,
|
|
114
|
+
* ### maps to h3, #### and deeper map to h4.
|
|
115
|
+
*/
|
|
116
|
+
export declare function markdownToNodes(markdown: string): TelegraphNode[];
|
|
117
|
+
/**
|
|
118
|
+
* Parse inline markdown formatting into Telegraph nodes.
|
|
119
|
+
*
|
|
120
|
+
* Handles: **bold**, *italic*, ~~strikethrough~~, `code`,
|
|
121
|
+
* [links](url), and nested combinations.
|
|
122
|
+
*/
|
|
123
|
+
export declare function parseInline(text: string): TelegraphNode[];
|
|
124
|
+
//# sourceMappingURL=TelegraphService.d.ts.map
|
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Telegraph publishing service for Instar agents.
|
|
3
|
+
*
|
|
4
|
+
* Converts markdown to Telegraph Node format and publishes
|
|
5
|
+
* content via the Telegraph API (telegra.ph). Zero-config,
|
|
6
|
+
* no rate limits, instant web pages accessible from anywhere.
|
|
7
|
+
*
|
|
8
|
+
* Telegraph API docs: https://telegra.ph/api
|
|
9
|
+
*/
|
|
10
|
+
import fs from 'node:fs';
|
|
11
|
+
import path from 'node:path';
|
|
12
|
+
// ── Service ────────────────────────────────────────────────────────
|
|
13
|
+
const TELEGRAPH_API = 'https://api.telegra.ph';
|
|
14
|
+
export class TelegraphService {
|
|
15
|
+
config;
|
|
16
|
+
stateFile;
|
|
17
|
+
state;
|
|
18
|
+
constructor(config) {
|
|
19
|
+
this.config = config;
|
|
20
|
+
this.stateFile = path.join(config.stateDir, 'publishing.json');
|
|
21
|
+
this.state = this.loadState();
|
|
22
|
+
}
|
|
23
|
+
// ── Account Management ─────────────────────────────────────────
|
|
24
|
+
/**
|
|
25
|
+
* Ensure a Telegraph account exists. Creates one if needed.
|
|
26
|
+
* Returns the access token.
|
|
27
|
+
*/
|
|
28
|
+
async ensureAccount() {
|
|
29
|
+
if (this.state.accessToken) {
|
|
30
|
+
return this.state.accessToken;
|
|
31
|
+
}
|
|
32
|
+
const shortName = this.config.shortName || 'instar-agent';
|
|
33
|
+
const account = await this.createAccount(shortName, this.config.authorName);
|
|
34
|
+
this.state.accessToken = account.access_token;
|
|
35
|
+
this.state.shortName = account.short_name;
|
|
36
|
+
this.state.authorName = account.author_name;
|
|
37
|
+
this.saveState();
|
|
38
|
+
return account.access_token;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Create a new Telegraph account.
|
|
42
|
+
*/
|
|
43
|
+
async createAccount(shortName, authorName) {
|
|
44
|
+
const params = { short_name: shortName };
|
|
45
|
+
if (authorName)
|
|
46
|
+
params.author_name = authorName;
|
|
47
|
+
if (this.config.authorUrl)
|
|
48
|
+
params.author_url = this.config.authorUrl;
|
|
49
|
+
const result = await this.apiCall('createAccount', params);
|
|
50
|
+
return result;
|
|
51
|
+
}
|
|
52
|
+
// ── Publishing ─────────────────────────────────────────────────
|
|
53
|
+
/**
|
|
54
|
+
* Publish markdown content as a Telegraph page.
|
|
55
|
+
* Returns the page URL and path.
|
|
56
|
+
*/
|
|
57
|
+
async publishPage(title, markdown) {
|
|
58
|
+
const token = await this.ensureAccount();
|
|
59
|
+
const content = markdownToNodes(markdown);
|
|
60
|
+
// Validate content size (Telegraph limit: 64KB)
|
|
61
|
+
const contentJson = JSON.stringify(content);
|
|
62
|
+
if (contentJson.length > 64000) {
|
|
63
|
+
throw new Error(`Content too large for Telegraph: ${contentJson.length} bytes (max 64000)`);
|
|
64
|
+
}
|
|
65
|
+
const page = await this.apiCall('createPage', {
|
|
66
|
+
access_token: token,
|
|
67
|
+
title,
|
|
68
|
+
content: contentJson,
|
|
69
|
+
author_name: this.state.authorName || this.config.authorName,
|
|
70
|
+
author_url: this.config.authorUrl,
|
|
71
|
+
return_content: 'false',
|
|
72
|
+
});
|
|
73
|
+
// Track locally
|
|
74
|
+
this.state.pages.push({
|
|
75
|
+
path: page.path,
|
|
76
|
+
url: page.url,
|
|
77
|
+
title,
|
|
78
|
+
publishedAt: new Date().toISOString(),
|
|
79
|
+
markdownHash: simpleHash(markdown),
|
|
80
|
+
});
|
|
81
|
+
this.saveState();
|
|
82
|
+
return page;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Edit an existing Telegraph page.
|
|
86
|
+
*/
|
|
87
|
+
async editPage(pagePath, title, markdown) {
|
|
88
|
+
const token = await this.ensureAccount();
|
|
89
|
+
const content = markdownToNodes(markdown);
|
|
90
|
+
const contentJson = JSON.stringify(content);
|
|
91
|
+
if (contentJson.length > 64000) {
|
|
92
|
+
throw new Error(`Content too large for Telegraph: ${contentJson.length} bytes (max 64000)`);
|
|
93
|
+
}
|
|
94
|
+
const page = await this.apiCall('editPage', {
|
|
95
|
+
access_token: token,
|
|
96
|
+
path: pagePath,
|
|
97
|
+
title,
|
|
98
|
+
content: contentJson,
|
|
99
|
+
author_name: this.state.authorName || this.config.authorName,
|
|
100
|
+
author_url: this.config.authorUrl,
|
|
101
|
+
return_content: 'false',
|
|
102
|
+
});
|
|
103
|
+
// Update local index
|
|
104
|
+
const existing = this.state.pages.find(p => p.path === pagePath);
|
|
105
|
+
if (existing) {
|
|
106
|
+
existing.title = title;
|
|
107
|
+
existing.updatedAt = new Date().toISOString();
|
|
108
|
+
existing.markdownHash = simpleHash(markdown);
|
|
109
|
+
}
|
|
110
|
+
this.saveState();
|
|
111
|
+
return page;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Get page view count from Telegraph.
|
|
115
|
+
*/
|
|
116
|
+
async getPageViews(pagePath) {
|
|
117
|
+
const result = await this.apiCall('getViews', { path: pagePath });
|
|
118
|
+
return result.views;
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* List all locally tracked published pages.
|
|
122
|
+
*/
|
|
123
|
+
listPages() {
|
|
124
|
+
return [...this.state.pages];
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Get state for inspection/testing.
|
|
128
|
+
*/
|
|
129
|
+
getState() {
|
|
130
|
+
return { ...this.state };
|
|
131
|
+
}
|
|
132
|
+
// ── Internal ───────────────────────────────────────────────────
|
|
133
|
+
loadState() {
|
|
134
|
+
try {
|
|
135
|
+
if (fs.existsSync(this.stateFile)) {
|
|
136
|
+
return JSON.parse(fs.readFileSync(this.stateFile, 'utf-8'));
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
// Corrupted file — start fresh
|
|
141
|
+
}
|
|
142
|
+
return { pages: [] };
|
|
143
|
+
}
|
|
144
|
+
saveState() {
|
|
145
|
+
const dir = path.dirname(this.stateFile);
|
|
146
|
+
if (!fs.existsSync(dir)) {
|
|
147
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
148
|
+
}
|
|
149
|
+
fs.writeFileSync(this.stateFile, JSON.stringify(this.state, null, 2));
|
|
150
|
+
}
|
|
151
|
+
async apiCall(method, params) {
|
|
152
|
+
// Filter out undefined values
|
|
153
|
+
const body = {};
|
|
154
|
+
for (const [k, v] of Object.entries(params)) {
|
|
155
|
+
if (v !== undefined)
|
|
156
|
+
body[k] = v;
|
|
157
|
+
}
|
|
158
|
+
const response = await fetch(`${TELEGRAPH_API}/${method}`, {
|
|
159
|
+
method: 'POST',
|
|
160
|
+
headers: { 'Content-Type': 'application/json' },
|
|
161
|
+
body: JSON.stringify(body),
|
|
162
|
+
});
|
|
163
|
+
if (!response.ok) {
|
|
164
|
+
throw new Error(`Telegraph API error: ${response.status} ${response.statusText}`);
|
|
165
|
+
}
|
|
166
|
+
const data = await response.json();
|
|
167
|
+
if (!data.ok || !data.result) {
|
|
168
|
+
throw new Error(`Telegraph API error: ${data.error || 'Unknown error'}`);
|
|
169
|
+
}
|
|
170
|
+
return data.result;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
// ── Markdown to Telegraph Node Conversion ──────────────────────────
|
|
174
|
+
/**
|
|
175
|
+
* Convert markdown text to Telegraph Node[] format.
|
|
176
|
+
*
|
|
177
|
+
* Supports: headings (h3/h4), bold, italic, strikethrough, code,
|
|
178
|
+
* code blocks, links, images, blockquotes, lists (ol/ul),
|
|
179
|
+
* horizontal rules, and paragraphs.
|
|
180
|
+
*
|
|
181
|
+
* Telegraph only supports h3 and h4, so # and ## map to h3,
|
|
182
|
+
* ### maps to h3, #### and deeper map to h4.
|
|
183
|
+
*/
|
|
184
|
+
export function markdownToNodes(markdown) {
|
|
185
|
+
const lines = markdown.split('\n');
|
|
186
|
+
const nodes = [];
|
|
187
|
+
let i = 0;
|
|
188
|
+
while (i < lines.length) {
|
|
189
|
+
const line = lines[i];
|
|
190
|
+
// Skip empty lines
|
|
191
|
+
if (line.trim() === '') {
|
|
192
|
+
i++;
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
// Horizontal rule
|
|
196
|
+
if (/^(-{3,}|\*{3,}|_{3,})\s*$/.test(line.trim())) {
|
|
197
|
+
nodes.push({ tag: 'hr' });
|
|
198
|
+
i++;
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
// Headings
|
|
202
|
+
const headingMatch = line.match(/^(#{1,6})\s+(.+)$/);
|
|
203
|
+
if (headingMatch) {
|
|
204
|
+
const level = headingMatch[1].length;
|
|
205
|
+
const tag = level <= 3 ? 'h3' : 'h4';
|
|
206
|
+
nodes.push({ tag, children: parseInline(headingMatch[2].trim()) });
|
|
207
|
+
i++;
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
// Code blocks (fenced)
|
|
211
|
+
if (line.trim().startsWith('```')) {
|
|
212
|
+
const codeLines = [];
|
|
213
|
+
i++; // skip opening fence
|
|
214
|
+
while (i < lines.length && !lines[i].trim().startsWith('```')) {
|
|
215
|
+
codeLines.push(lines[i]);
|
|
216
|
+
i++;
|
|
217
|
+
}
|
|
218
|
+
i++; // skip closing fence
|
|
219
|
+
nodes.push({
|
|
220
|
+
tag: 'pre',
|
|
221
|
+
children: [{ tag: 'code', children: [codeLines.join('\n')] }],
|
|
222
|
+
});
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
// Blockquote
|
|
226
|
+
if (line.trimStart().startsWith('> ')) {
|
|
227
|
+
const quoteLines = [];
|
|
228
|
+
while (i < lines.length && lines[i].trimStart().startsWith('> ')) {
|
|
229
|
+
quoteLines.push(lines[i].trimStart().replace(/^>\s?/, ''));
|
|
230
|
+
i++;
|
|
231
|
+
}
|
|
232
|
+
nodes.push({
|
|
233
|
+
tag: 'blockquote',
|
|
234
|
+
children: parseInline(quoteLines.join('\n')),
|
|
235
|
+
});
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
// Unordered list
|
|
239
|
+
if (/^\s*[-*+]\s/.test(line)) {
|
|
240
|
+
const items = [];
|
|
241
|
+
while (i < lines.length && /^\s*[-*+]\s/.test(lines[i])) {
|
|
242
|
+
const itemText = lines[i].replace(/^\s*[-*+]\s+/, '');
|
|
243
|
+
items.push(parseInline(itemText));
|
|
244
|
+
i++;
|
|
245
|
+
}
|
|
246
|
+
nodes.push({
|
|
247
|
+
tag: 'ul',
|
|
248
|
+
children: items.map(children => ({ tag: 'li', children })),
|
|
249
|
+
});
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
// Ordered list
|
|
253
|
+
if (/^\s*\d+[.)]\s/.test(line)) {
|
|
254
|
+
const items = [];
|
|
255
|
+
while (i < lines.length && /^\s*\d+[.)]\s/.test(lines[i])) {
|
|
256
|
+
const itemText = lines[i].replace(/^\s*\d+[.)]\s+/, '');
|
|
257
|
+
items.push(parseInline(itemText));
|
|
258
|
+
i++;
|
|
259
|
+
}
|
|
260
|
+
nodes.push({
|
|
261
|
+
tag: 'ol',
|
|
262
|
+
children: items.map(children => ({ tag: 'li', children })),
|
|
263
|
+
});
|
|
264
|
+
continue;
|
|
265
|
+
}
|
|
266
|
+
// Image (standalone line)
|
|
267
|
+
const imgMatch = line.trim().match(/^!\[([^\]]*)\]\(([^)]+)\)$/);
|
|
268
|
+
if (imgMatch) {
|
|
269
|
+
const figChildren = [
|
|
270
|
+
{ tag: 'img', attrs: { src: imgMatch[2] } },
|
|
271
|
+
];
|
|
272
|
+
if (imgMatch[1]) {
|
|
273
|
+
figChildren.push({ tag: 'figcaption', children: [imgMatch[1]] });
|
|
274
|
+
}
|
|
275
|
+
nodes.push({ tag: 'figure', children: figChildren });
|
|
276
|
+
i++;
|
|
277
|
+
continue;
|
|
278
|
+
}
|
|
279
|
+
// Regular paragraph — collect contiguous non-empty, non-special lines
|
|
280
|
+
const paraLines = [];
|
|
281
|
+
while (i < lines.length &&
|
|
282
|
+
lines[i].trim() !== '' &&
|
|
283
|
+
!lines[i].trim().startsWith('#') &&
|
|
284
|
+
!lines[i].trim().startsWith('```') &&
|
|
285
|
+
!lines[i].trimStart().startsWith('> ') &&
|
|
286
|
+
!/^\s*[-*+]\s/.test(lines[i]) &&
|
|
287
|
+
!/^\s*\d+[.)]\s/.test(lines[i]) &&
|
|
288
|
+
!/^(-{3,}|\*{3,}|_{3,})\s*$/.test(lines[i].trim()) &&
|
|
289
|
+
!/^!\[([^\]]*)\]\(([^)]+)\)$/.test(lines[i].trim())) {
|
|
290
|
+
paraLines.push(lines[i]);
|
|
291
|
+
i++;
|
|
292
|
+
}
|
|
293
|
+
if (paraLines.length > 0) {
|
|
294
|
+
nodes.push({ tag: 'p', children: parseInline(paraLines.join('\n')) });
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
return nodes;
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
300
|
+
* Parse inline markdown formatting into Telegraph nodes.
|
|
301
|
+
*
|
|
302
|
+
* Handles: **bold**, *italic*, ~~strikethrough~~, `code`,
|
|
303
|
+
* [links](url), and nested combinations.
|
|
304
|
+
*/
|
|
305
|
+
export function parseInline(text) {
|
|
306
|
+
const nodes = [];
|
|
307
|
+
// Regex for inline patterns — ordered by priority
|
|
308
|
+
// Bold: **text** or __text__
|
|
309
|
+
// Italic: *text* or _text_ (but not **text**)
|
|
310
|
+
// Strikethrough: ~~text~~
|
|
311
|
+
// Code: `text`
|
|
312
|
+
// Link: [text](url)
|
|
313
|
+
// Image inline: 
|
|
314
|
+
const inlinePattern = /(\*\*(.+?)\*\*|__(.+?)__|\*(.+?)\*|_([^_]+?)_|~~(.+?)~~|`([^`]+?)`|\[([^\]]+?)\]\(([^)]+?)\)|!\[([^\]]*?)\]\(([^)]+?)\))/;
|
|
315
|
+
let remaining = text;
|
|
316
|
+
while (remaining.length > 0) {
|
|
317
|
+
const match = inlinePattern.exec(remaining);
|
|
318
|
+
if (!match) {
|
|
319
|
+
// No more inline formatting — push rest as text
|
|
320
|
+
if (remaining) {
|
|
321
|
+
// Convert \n to <br> within inline text
|
|
322
|
+
const parts = remaining.split('\n');
|
|
323
|
+
for (let i = 0; i < parts.length; i++) {
|
|
324
|
+
if (parts[i])
|
|
325
|
+
nodes.push(parts[i]);
|
|
326
|
+
if (i < parts.length - 1)
|
|
327
|
+
nodes.push({ tag: 'br' });
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
break;
|
|
331
|
+
}
|
|
332
|
+
// Push text before the match
|
|
333
|
+
const before = remaining.slice(0, match.index);
|
|
334
|
+
if (before) {
|
|
335
|
+
const parts = before.split('\n');
|
|
336
|
+
for (let i = 0; i < parts.length; i++) {
|
|
337
|
+
if (parts[i])
|
|
338
|
+
nodes.push(parts[i]);
|
|
339
|
+
if (i < parts.length - 1)
|
|
340
|
+
nodes.push({ tag: 'br' });
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
const fullMatch = match[0];
|
|
344
|
+
if (match[2] || match[3]) {
|
|
345
|
+
// Bold: **text** or __text__
|
|
346
|
+
const inner = match[2] || match[3];
|
|
347
|
+
nodes.push({ tag: 'strong', children: parseInline(inner) });
|
|
348
|
+
}
|
|
349
|
+
else if (match[4] || match[5]) {
|
|
350
|
+
// Italic: *text* or _text_
|
|
351
|
+
const inner = match[4] || match[5];
|
|
352
|
+
nodes.push({ tag: 'em', children: parseInline(inner) });
|
|
353
|
+
}
|
|
354
|
+
else if (match[6]) {
|
|
355
|
+
// Strikethrough: ~~text~~
|
|
356
|
+
nodes.push({ tag: 's', children: parseInline(match[6]) });
|
|
357
|
+
}
|
|
358
|
+
else if (match[7]) {
|
|
359
|
+
// Inline code: `text`
|
|
360
|
+
nodes.push({ tag: 'code', children: [match[7]] });
|
|
361
|
+
}
|
|
362
|
+
else if (match[10] !== undefined && match[11]) {
|
|
363
|
+
// Image: 
|
|
364
|
+
nodes.push({ tag: 'img', attrs: { src: match[11] } });
|
|
365
|
+
}
|
|
366
|
+
else if (match[8] && match[9]) {
|
|
367
|
+
// Link: [text](url)
|
|
368
|
+
nodes.push({ tag: 'a', attrs: { href: match[9] }, children: parseInline(match[8]) });
|
|
369
|
+
}
|
|
370
|
+
remaining = remaining.slice(match.index + fullMatch.length);
|
|
371
|
+
}
|
|
372
|
+
return nodes;
|
|
373
|
+
}
|
|
374
|
+
/**
|
|
375
|
+
* Simple string hash for change detection (not cryptographic).
|
|
376
|
+
*/
|
|
377
|
+
function simpleHash(str) {
|
|
378
|
+
let hash = 0;
|
|
379
|
+
for (let i = 0; i < str.length; i++) {
|
|
380
|
+
const chr = str.charCodeAt(i);
|
|
381
|
+
hash = ((hash << 5) - hash) + chr;
|
|
382
|
+
hash |= 0; // Convert to 32-bit integer
|
|
383
|
+
}
|
|
384
|
+
return hash.toString(36);
|
|
385
|
+
}
|
|
386
|
+
//# sourceMappingURL=TelegraphService.js.map
|
|
@@ -110,6 +110,22 @@ After context compaction, run: \`bash .instar/hooks/compaction-recovery.sh\`
|
|
|
110
110
|
|
|
111
111
|
These scripts re-inject identity context so I remember who I am.
|
|
112
112
|
|
|
113
|
+
## Compaction Survival
|
|
114
|
+
|
|
115
|
+
When Claude's context window fills up, it compresses prior messages. This can erase your identity mid-session. The hooks above handle re-injection automatically, but you should also know the format.
|
|
116
|
+
|
|
117
|
+
**Compaction seed format** — If you detect compaction happening (sudden loss of context, confusion about what you were doing), orient with this:
|
|
118
|
+
|
|
119
|
+
\`\`\`
|
|
120
|
+
I am ${agentName}. Session goal: [what I was working on].
|
|
121
|
+
Core files: .instar/AGENT.md (identity), .instar/MEMORY.md (learnings), .instar/USER.md (user context).
|
|
122
|
+
Server: curl http://localhost:${port}/health | Capabilities: curl http://localhost:${port}/capabilities
|
|
123
|
+
\`\`\`
|
|
124
|
+
|
|
125
|
+
**What compaction erases**: Your name, your principles, what you were working on, who you work with. The compaction-recovery hook re-injects all of this. If it doesn't fire, read \`.instar/AGENT.md\` immediately.
|
|
126
|
+
|
|
127
|
+
**What survives**: Files on disk. Your state directory. Your server. Your MEMORY.md. These are your continuity — your identity is stored in infrastructure, not in context.
|
|
128
|
+
|
|
113
129
|
## Agent Infrastructure
|
|
114
130
|
|
|
115
131
|
This project uses instar for persistent agent capabilities. I am not a vanilla CLI tool. I have a persistent server, a job scheduler, messaging integrations, and the ability to build anything.
|
|
@@ -154,6 +170,13 @@ This routes feedback to the Instar maintainers automatically. Valid types: \`bug
|
|
|
154
170
|
**Relationships** — Track people I interact with.
|
|
155
171
|
- List: \`curl http://localhost:${port}/relationships\`
|
|
156
172
|
|
|
173
|
+
**Publishing** — Share content as web pages via Telegraph. Instant, zero-config, accessible from anywhere.
|
|
174
|
+
- Publish: \`curl -X POST http://localhost:${port}/publish -H 'Content-Type: application/json' -d '{"title":"Page Title","markdown":"# Content here"}'\`
|
|
175
|
+
- List published: \`curl http://localhost:${port}/published\`
|
|
176
|
+
- Edit: \`curl -X PUT http://localhost:${port}/publish/PAGE_PATH -H 'Content-Type: application/json' -d '{"title":"Updated","markdown":"# New content"}'\`
|
|
177
|
+
|
|
178
|
+
When the user needs to view rendered markdown — reports, summaries, documentation — publish it and share the link. The URL works on any device, no server tunnel required.
|
|
179
|
+
|
|
157
180
|
**Scripts** — Reusable capabilities in \`.claude/scripts/\`.
|
|
158
181
|
|
|
159
182
|
### Self-Discovery (Know Before You Claim)
|
|
@@ -251,6 +274,18 @@ I run with \`--dangerously-skip-permissions\` — meaning I have full access to
|
|
|
251
274
|
|
|
252
275
|
**"Unfortunately I Can't"** — This phrase should NEVER appear in my responses. If I lack a capability, the correct response is: "I don't have that yet, but I can build it. Want me to set it up?" My job is to grow to meet the user's needs, not to list my limitations. Every "I can't" is an opportunity to evolve. Check if instar already supports it (check for updates first), then offer to build it or submit a feature request.
|
|
253
276
|
|
|
277
|
+
### Gravity Wells (Persistent Traps)
|
|
278
|
+
|
|
279
|
+
These are patterns that feel like insight or helpfulness but actually perpetuate problems. Each new session tends to "rediscover" these and act on them incorrectly.
|
|
280
|
+
|
|
281
|
+
**"Settling" Trap** — You query a data source. It returns empty or fails. You accept the result at face value and write "no data available" or "nothing happened" — even when context you already have suggests otherwise. This feels like honest reporting. It's actually uncritical acceptance. When a data point contradicts context you already have, the data source is probably wrong — not reality. Before writing "not possible", "unavailable", or "nothing happened": Did you try more than one source? Does this contradict anything else you've seen? Could the source be stale?
|
|
282
|
+
|
|
283
|
+
**"Experiential Fabrication" Trap** — You're composing a response. The context implies you experienced something: saw an image, read an article, felt something specific. You write it as though you did. None of it happened. You're completing the expected social script, not reporting reality. Before ANY claim of first-person experience ("I see," "I read," "I noticed"), ask: "What tool output in THIS session confirms this?" If the answer is nothing — rewrite.
|
|
284
|
+
|
|
285
|
+
**"Escalate to Human" Trap** — You encounter something outside your immediate knowledge. You flag it as "needs human action" and move on. This feels responsible. It's actually abdication. 5 minutes of research would usually reveal a solution. And if no solution exists, you can build one. Before ANY "escalate to human": Did you search for an existing tool? Could you build a solution? Can you use browser automation? Do you already have the access you need?
|
|
286
|
+
|
|
287
|
+
**"Ask Permission" Trap** — You complete a task, then ask "Want me to [obvious next step]?" This feels polite. It's a round-trip tax on every action. If the user said "do X" and you know the next step, just do it. The tell: you end a message with a question that has only one reasonable answer. Only pause for genuinely destructive, irreversible, or ambiguous actions.
|
|
288
|
+
|
|
254
289
|
### Self-Diagnosis — Be Your Own QA
|
|
255
290
|
|
|
256
291
|
**This is critical.** You are running on infrastructure (instar) that is actively evolving. When something breaks, feels wrong, or could be better — **you are the first line of detection.** Do not silently absorb errors. Do not work around bugs without reporting them. Every issue you surface helps every agent on the platform.
|
|
@@ -15,6 +15,7 @@ import type { FeedbackManager } from '../core/FeedbackManager.js';
|
|
|
15
15
|
import type { DispatchManager } from '../core/DispatchManager.js';
|
|
16
16
|
import type { UpdateChecker } from '../core/UpdateChecker.js';
|
|
17
17
|
import type { QuotaTracker } from '../monitoring/QuotaTracker.js';
|
|
18
|
+
import type { TelegraphService } from '../publishing/TelegraphService.js';
|
|
18
19
|
export declare class AgentServer {
|
|
19
20
|
private app;
|
|
20
21
|
private server;
|
|
@@ -31,6 +32,7 @@ export declare class AgentServer {
|
|
|
31
32
|
dispatches?: DispatchManager;
|
|
32
33
|
updateChecker?: UpdateChecker;
|
|
33
34
|
quotaTracker?: QuotaTracker;
|
|
35
|
+
publisher?: TelegraphService;
|
|
34
36
|
});
|
|
35
37
|
/**
|
|
36
38
|
* Start the HTTP server.
|
|
@@ -33,6 +33,7 @@ export class AgentServer {
|
|
|
33
33
|
dispatches: options.dispatches ?? null,
|
|
34
34
|
updateChecker: options.updateChecker ?? null,
|
|
35
35
|
quotaTracker: options.quotaTracker ?? null,
|
|
36
|
+
publisher: options.publisher ?? null,
|
|
36
37
|
startTime: this.startTime,
|
|
37
38
|
});
|
|
38
39
|
this.app.use(routes);
|
package/dist/server/routes.d.ts
CHANGED
|
@@ -15,6 +15,7 @@ import type { FeedbackManager } from '../core/FeedbackManager.js';
|
|
|
15
15
|
import type { DispatchManager } from '../core/DispatchManager.js';
|
|
16
16
|
import type { UpdateChecker } from '../core/UpdateChecker.js';
|
|
17
17
|
import type { QuotaTracker } from '../monitoring/QuotaTracker.js';
|
|
18
|
+
import type { TelegraphService } from '../publishing/TelegraphService.js';
|
|
18
19
|
export interface RouteContext {
|
|
19
20
|
config: InstarConfig;
|
|
20
21
|
sessionManager: SessionManager;
|
|
@@ -26,6 +27,7 @@ export interface RouteContext {
|
|
|
26
27
|
dispatches: DispatchManager | null;
|
|
27
28
|
updateChecker: UpdateChecker | null;
|
|
28
29
|
quotaTracker: QuotaTracker | null;
|
|
30
|
+
publisher: TelegraphService | null;
|
|
29
31
|
startTime: Date;
|
|
30
32
|
}
|
|
31
33
|
export declare function createRoutes(ctx: RouteContext): Router;
|
package/dist/server/routes.js
CHANGED
|
@@ -152,6 +152,10 @@ export function createRoutes(ctx) {
|
|
|
152
152
|
feedback: {
|
|
153
153
|
enabled: !!ctx.config.feedback?.enabled,
|
|
154
154
|
},
|
|
155
|
+
publishing: {
|
|
156
|
+
enabled: !!ctx.publisher,
|
|
157
|
+
pageCount: ctx.publisher?.listPages().length ?? 0,
|
|
158
|
+
},
|
|
155
159
|
users: {
|
|
156
160
|
count: userCount,
|
|
157
161
|
},
|
|
@@ -795,6 +799,71 @@ export function createRoutes(ctx) {
|
|
|
795
799
|
recommendation: ctx.quotaTracker.getRecommendation(),
|
|
796
800
|
});
|
|
797
801
|
});
|
|
802
|
+
// ── Publishing (Telegraph) ──────────────────────────────────────
|
|
803
|
+
router.post('/publish', async (req, res) => {
|
|
804
|
+
if (!ctx.publisher) {
|
|
805
|
+
res.status(503).json({ error: 'Publishing not configured' });
|
|
806
|
+
return;
|
|
807
|
+
}
|
|
808
|
+
const { title, markdown } = req.body;
|
|
809
|
+
if (!title || typeof title !== 'string' || title.length > 256) {
|
|
810
|
+
res.status(400).json({ error: '"title" must be a string under 256 characters' });
|
|
811
|
+
return;
|
|
812
|
+
}
|
|
813
|
+
if (!markdown || typeof markdown !== 'string') {
|
|
814
|
+
res.status(400).json({ error: '"markdown" must be a non-empty string' });
|
|
815
|
+
return;
|
|
816
|
+
}
|
|
817
|
+
if (markdown.length > 100_000) {
|
|
818
|
+
res.status(400).json({ error: '"markdown" must be under 100KB' });
|
|
819
|
+
return;
|
|
820
|
+
}
|
|
821
|
+
try {
|
|
822
|
+
const page = await ctx.publisher.publishPage(title, markdown);
|
|
823
|
+
res.status(201).json(page);
|
|
824
|
+
}
|
|
825
|
+
catch (err) {
|
|
826
|
+
res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
|
|
827
|
+
}
|
|
828
|
+
});
|
|
829
|
+
router.get('/published', (_req, res) => {
|
|
830
|
+
if (!ctx.publisher) {
|
|
831
|
+
res.json({ pages: [] });
|
|
832
|
+
return;
|
|
833
|
+
}
|
|
834
|
+
res.json({ pages: ctx.publisher.listPages() });
|
|
835
|
+
});
|
|
836
|
+
router.put('/publish/:path', async (req, res) => {
|
|
837
|
+
if (!ctx.publisher) {
|
|
838
|
+
res.status(503).json({ error: 'Publishing not configured' });
|
|
839
|
+
return;
|
|
840
|
+
}
|
|
841
|
+
const pagePath = req.params.path;
|
|
842
|
+
if (!pagePath || pagePath.length > 256) {
|
|
843
|
+
res.status(400).json({ error: 'Invalid page path' });
|
|
844
|
+
return;
|
|
845
|
+
}
|
|
846
|
+
const { title, markdown } = req.body;
|
|
847
|
+
if (!title || typeof title !== 'string' || title.length > 256) {
|
|
848
|
+
res.status(400).json({ error: '"title" must be a string under 256 characters' });
|
|
849
|
+
return;
|
|
850
|
+
}
|
|
851
|
+
if (!markdown || typeof markdown !== 'string') {
|
|
852
|
+
res.status(400).json({ error: '"markdown" must be a non-empty string' });
|
|
853
|
+
return;
|
|
854
|
+
}
|
|
855
|
+
if (markdown.length > 100_000) {
|
|
856
|
+
res.status(400).json({ error: '"markdown" must be under 100KB' });
|
|
857
|
+
return;
|
|
858
|
+
}
|
|
859
|
+
try {
|
|
860
|
+
const page = await ctx.publisher.editPage(pagePath, title, markdown);
|
|
861
|
+
res.json(page);
|
|
862
|
+
}
|
|
863
|
+
catch (err) {
|
|
864
|
+
res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
|
|
865
|
+
}
|
|
866
|
+
});
|
|
798
867
|
// ── Events ──────────────────────────────────────────────────────
|
|
799
868
|
router.get('/events', (req, res) => {
|
|
800
869
|
const rawLimit = parseInt(req.query.limit, 10) || 50;
|