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 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: {
@@ -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
- if (telegramConfig) {
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
- const server = new AgentServer({ config, sessionManager, state, scheduler, telegram, relationships, feedback, dispatches, updateChecker });
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 () => {
@@ -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 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, } from './core/types.js';
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
- const nodeCmd = ['node', cliPath, 'server', 'start', '--foreground']
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: ![alt](src)
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: ![alt](src)
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);
@@ -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;
@@ -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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "instar",
3
- "version": "0.4.10",
3
+ "version": "0.5.0",
4
4
  "description": "Persistent autonomy infrastructure for AI agents",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",