@agi-cli/server 0.1.138 → 0.1.139

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agi-cli/server",
3
- "version": "0.1.138",
3
+ "version": "0.1.139",
4
4
  "description": "HTTP API server for AGI CLI",
5
5
  "type": "module",
6
6
  "main": "./src/index.ts",
@@ -29,8 +29,8 @@
29
29
  "typecheck": "tsc --noEmit"
30
30
  },
31
31
  "dependencies": {
32
- "@agi-cli/sdk": "0.1.138",
33
- "@agi-cli/database": "0.1.138",
32
+ "@agi-cli/sdk": "0.1.139",
33
+ "@agi-cli/database": "0.1.139",
34
34
  "drizzle-orm": "^0.44.5",
35
35
  "hono": "^4.9.9",
36
36
  "zod": "^4.1.8"
@@ -1,7 +1,13 @@
1
1
  import type { Hono } from 'hono';
2
2
  import { loadConfig } from '@agi-cli/sdk';
3
+ import { userInfo } from 'node:os';
3
4
  import { getDb } from '@agi-cli/database';
4
- import { sessions, messages, messageParts } from '@agi-cli/database/schema';
5
+ import {
6
+ sessions,
7
+ messages,
8
+ messageParts,
9
+ shares,
10
+ } from '@agi-cli/database/schema';
5
11
  import { desc, eq, and, ne, inArray } from 'drizzle-orm';
6
12
  import type { ProviderId } from '@agi-cli/sdk';
7
13
  import { isProviderId, catalog } from '@agi-cli/sdk';
@@ -196,6 +202,52 @@ export function registerSessionsRoutes(app: Hono) {
196
202
  }
197
203
  });
198
204
 
205
+ // Delete session
206
+ app.delete('/v1/sessions/:sessionId', async (c) => {
207
+ try {
208
+ const sessionId = c.req.param('sessionId');
209
+ const projectRoot = c.req.query('project') || process.cwd();
210
+ const cfg = await loadConfig(projectRoot);
211
+ const db = await getDb(cfg.projectRoot);
212
+
213
+ const existingRows = await db
214
+ .select()
215
+ .from(sessions)
216
+ .where(eq(sessions.id, sessionId))
217
+ .limit(1);
218
+
219
+ if (!existingRows.length) {
220
+ return c.json({ error: 'Session not found' }, 404);
221
+ }
222
+
223
+ const existingSession = existingRows[0];
224
+
225
+ if (existingSession.projectPath !== cfg.projectRoot) {
226
+ return c.json({ error: 'Session not found in this project' }, 404);
227
+ }
228
+
229
+ await db
230
+ .delete(messageParts)
231
+ .where(
232
+ inArray(
233
+ messageParts.messageId,
234
+ db
235
+ .select({ id: messages.id })
236
+ .from(messages)
237
+ .where(eq(messages.sessionId, sessionId)),
238
+ ),
239
+ );
240
+ await db.delete(messages).where(eq(messages.sessionId, sessionId));
241
+ await db.delete(sessions).where(eq(sessions.id, sessionId));
242
+
243
+ return c.json({ success: true });
244
+ } catch (err) {
245
+ logger.error('Failed to delete session', err);
246
+ const errorResponse = serializeError(err);
247
+ return c.json(errorResponse, errorResponse.error.status || 500);
248
+ }
249
+ });
250
+
199
251
  // Abort session stream
200
252
  app.delete('/v1/sessions/:sessionId/abort', async (c) => {
201
253
  const sessionId = c.req.param('sessionId');
@@ -331,4 +383,317 @@ export function registerSessionsRoutes(app: Hono) {
331
383
 
332
384
  return c.json({ success: false, removed: false }, 404);
333
385
  });
386
+
387
+ app.get('/v1/sessions/:sessionId/share', async (c) => {
388
+ const sessionId = c.req.param('sessionId');
389
+ const projectRoot = c.req.query('project') || process.cwd();
390
+ const cfg = await loadConfig(projectRoot);
391
+ const db = await getDb(cfg.projectRoot);
392
+
393
+ const share = await db
394
+ .select()
395
+ .from(shares)
396
+ .where(eq(shares.sessionId, sessionId))
397
+ .limit(1);
398
+
399
+ if (!share.length) {
400
+ return c.json({ shared: false });
401
+ }
402
+
403
+ const allMessages = await db
404
+ .select({ id: messages.id })
405
+ .from(messages)
406
+ .where(eq(messages.sessionId, sessionId))
407
+ .orderBy(messages.createdAt);
408
+
409
+ const totalMessages = allMessages.length;
410
+ const syncedIdx = allMessages.findIndex(
411
+ (m) => m.id === share[0].lastSyncedMessageId,
412
+ );
413
+ const syncedMessages = syncedIdx === -1 ? 0 : syncedIdx + 1;
414
+ const pendingMessages = totalMessages - syncedMessages;
415
+
416
+ return c.json({
417
+ shared: true,
418
+ shareId: share[0].shareId,
419
+ url: share[0].url,
420
+ title: share[0].title,
421
+ createdAt: share[0].createdAt,
422
+ lastSyncedAt: share[0].lastSyncedAt,
423
+ lastSyncedMessageId: share[0].lastSyncedMessageId,
424
+ syncedMessages,
425
+ totalMessages,
426
+ pendingMessages,
427
+ isSynced: pendingMessages === 0,
428
+ });
429
+ });
430
+
431
+ const SHARE_API_URL =
432
+ process.env.AGI_SHARE_API_URL || 'https://api.share.agi.nitish.sh';
433
+
434
+ function getUsername(): string {
435
+ try {
436
+ return userInfo().username;
437
+ } catch {
438
+ return 'anonymous';
439
+ }
440
+ }
441
+
442
+ app.post('/v1/sessions/:sessionId/share', async (c) => {
443
+ const sessionId = c.req.param('sessionId');
444
+ const projectRoot = c.req.query('project') || process.cwd();
445
+ const cfg = await loadConfig(projectRoot);
446
+ const db = await getDb(cfg.projectRoot);
447
+
448
+ const session = await db
449
+ .select()
450
+ .from(sessions)
451
+ .where(eq(sessions.id, sessionId))
452
+ .limit(1);
453
+ if (!session.length) {
454
+ return c.json({ error: 'Session not found' }, 404);
455
+ }
456
+
457
+ const existingShare = await db
458
+ .select()
459
+ .from(shares)
460
+ .where(eq(shares.sessionId, sessionId))
461
+ .limit(1);
462
+ if (existingShare.length) {
463
+ return c.json({
464
+ shared: true,
465
+ shareId: existingShare[0].shareId,
466
+ url: existingShare[0].url,
467
+ message: 'Already shared',
468
+ });
469
+ }
470
+
471
+ const allMessages = await db
472
+ .select()
473
+ .from(messages)
474
+ .where(eq(messages.sessionId, sessionId))
475
+ .orderBy(messages.createdAt);
476
+
477
+ if (!allMessages.length) {
478
+ return c.json({ error: 'Session has no messages' }, 400);
479
+ }
480
+
481
+ const msgParts = await db
482
+ .select()
483
+ .from(messageParts)
484
+ .where(
485
+ inArray(
486
+ messageParts.messageId,
487
+ allMessages.map((m) => m.id),
488
+ ),
489
+ )
490
+ .orderBy(messageParts.index);
491
+
492
+ const partsByMessage = new Map<string, typeof msgParts>();
493
+ for (const part of msgParts) {
494
+ const list = partsByMessage.get(part.messageId) || [];
495
+ list.push(part);
496
+ partsByMessage.set(part.messageId, list);
497
+ }
498
+
499
+ const lastMessageId = allMessages[allMessages.length - 1].id;
500
+ const sess = session[0];
501
+
502
+ const sessionData = {
503
+ title: sess.title,
504
+ username: getUsername(),
505
+ agent: sess.agent,
506
+ provider: sess.provider,
507
+ model: sess.model,
508
+ createdAt: sess.createdAt,
509
+ stats: {
510
+ inputTokens: sess.totalInputTokens ?? 0,
511
+ outputTokens: sess.totalOutputTokens ?? 0,
512
+ cachedTokens: sess.totalCachedTokens ?? 0,
513
+ cacheCreationTokens: sess.totalCacheCreationTokens ?? 0,
514
+ reasoningTokens: sess.totalReasoningTokens ?? 0,
515
+ toolTimeMs: sess.totalToolTimeMs ?? 0,
516
+ toolCounts: sess.toolCountsJson ? JSON.parse(sess.toolCountsJson) : {},
517
+ },
518
+ messages: allMessages.map((m) => ({
519
+ id: m.id,
520
+ role: m.role,
521
+ createdAt: m.createdAt,
522
+ parts: (partsByMessage.get(m.id) || []).map((p) => ({
523
+ type: p.type,
524
+ content: p.content,
525
+ toolName: p.toolName,
526
+ toolCallId: p.toolCallId,
527
+ })),
528
+ })),
529
+ };
530
+
531
+ const res = await fetch(`${SHARE_API_URL}/share`, {
532
+ method: 'POST',
533
+ headers: { 'Content-Type': 'application/json' },
534
+ body: JSON.stringify({
535
+ sessionData,
536
+ title: sess.title,
537
+ lastMessageId,
538
+ }),
539
+ });
540
+
541
+ if (!res.ok) {
542
+ const err = await res.text();
543
+ return c.json({ error: `Failed to create share: ${err}` }, 500);
544
+ }
545
+
546
+ const data = (await res.json()) as {
547
+ shareId: string;
548
+ secret: string;
549
+ url: string;
550
+ };
551
+
552
+ await db.insert(shares).values({
553
+ sessionId,
554
+ shareId: data.shareId,
555
+ secret: data.secret,
556
+ url: data.url,
557
+ title: sess.title,
558
+ description: null,
559
+ createdAt: Date.now(),
560
+ lastSyncedAt: Date.now(),
561
+ lastSyncedMessageId: lastMessageId,
562
+ });
563
+
564
+ return c.json({
565
+ shared: true,
566
+ shareId: data.shareId,
567
+ url: data.url,
568
+ });
569
+ });
570
+
571
+ app.put('/v1/sessions/:sessionId/share', async (c) => {
572
+ const sessionId = c.req.param('sessionId');
573
+ const projectRoot = c.req.query('project') || process.cwd();
574
+ const cfg = await loadConfig(projectRoot);
575
+ const db = await getDb(cfg.projectRoot);
576
+
577
+ const share = await db
578
+ .select()
579
+ .from(shares)
580
+ .where(eq(shares.sessionId, sessionId))
581
+ .limit(1);
582
+ if (!share.length) {
583
+ return c.json({ error: 'Session not shared. Use share first.' }, 400);
584
+ }
585
+
586
+ const session = await db
587
+ .select()
588
+ .from(sessions)
589
+ .where(eq(sessions.id, sessionId))
590
+ .limit(1);
591
+ if (!session.length) {
592
+ return c.json({ error: 'Session not found' }, 404);
593
+ }
594
+
595
+ const allMessages = await db
596
+ .select()
597
+ .from(messages)
598
+ .where(eq(messages.sessionId, sessionId))
599
+ .orderBy(messages.createdAt);
600
+
601
+ const msgParts = await db
602
+ .select()
603
+ .from(messageParts)
604
+ .where(
605
+ inArray(
606
+ messageParts.messageId,
607
+ allMessages.map((m) => m.id),
608
+ ),
609
+ )
610
+ .orderBy(messageParts.index);
611
+
612
+ const partsByMessage = new Map<string, typeof msgParts>();
613
+ for (const part of msgParts) {
614
+ const list = partsByMessage.get(part.messageId) || [];
615
+ list.push(part);
616
+ partsByMessage.set(part.messageId, list);
617
+ }
618
+
619
+ const lastSyncedIdx = allMessages.findIndex(
620
+ (m) => m.id === share[0].lastSyncedMessageId,
621
+ );
622
+ const newMessages =
623
+ lastSyncedIdx === -1 ? allMessages : allMessages.slice(lastSyncedIdx + 1);
624
+ const lastMessageId =
625
+ allMessages[allMessages.length - 1]?.id ?? share[0].lastSyncedMessageId;
626
+
627
+ if (newMessages.length === 0) {
628
+ return c.json({
629
+ synced: true,
630
+ url: share[0].url,
631
+ newMessages: 0,
632
+ message: 'Already synced',
633
+ });
634
+ }
635
+
636
+ const sess = session[0];
637
+ const sessionData = {
638
+ title: sess.title,
639
+ username: getUsername(),
640
+ agent: sess.agent,
641
+ provider: sess.provider,
642
+ model: sess.model,
643
+ createdAt: sess.createdAt,
644
+ stats: {
645
+ inputTokens: sess.totalInputTokens ?? 0,
646
+ outputTokens: sess.totalOutputTokens ?? 0,
647
+ cachedTokens: sess.totalCachedTokens ?? 0,
648
+ cacheCreationTokens: sess.totalCacheCreationTokens ?? 0,
649
+ reasoningTokens: sess.totalReasoningTokens ?? 0,
650
+ toolTimeMs: sess.totalToolTimeMs ?? 0,
651
+ toolCounts: sess.toolCountsJson ? JSON.parse(sess.toolCountsJson) : {},
652
+ },
653
+ messages: allMessages.map((m) => ({
654
+ id: m.id,
655
+ role: m.role,
656
+ createdAt: m.createdAt,
657
+ parts: (partsByMessage.get(m.id) || []).map((p) => ({
658
+ type: p.type,
659
+ content: p.content,
660
+ toolName: p.toolName,
661
+ toolCallId: p.toolCallId,
662
+ })),
663
+ })),
664
+ };
665
+
666
+ const res = await fetch(`${SHARE_API_URL}/share/${share[0].shareId}`, {
667
+ method: 'PUT',
668
+ headers: {
669
+ 'Content-Type': 'application/json',
670
+ 'X-Share-Secret': share[0].secret,
671
+ },
672
+ body: JSON.stringify({
673
+ sessionData,
674
+ title: sess.title,
675
+ lastMessageId,
676
+ }),
677
+ });
678
+
679
+ if (!res.ok) {
680
+ const err = await res.text();
681
+ return c.json({ error: `Failed to sync share: ${err}` }, 500);
682
+ }
683
+
684
+ await db
685
+ .update(shares)
686
+ .set({
687
+ title: sess.title,
688
+ lastSyncedAt: Date.now(),
689
+ lastSyncedMessageId: lastMessageId,
690
+ })
691
+ .where(eq(shares.sessionId, sessionId));
692
+
693
+ return c.json({
694
+ synced: true,
695
+ url: share[0].url,
696
+ newMessages: newMessages.length,
697
+ });
698
+ });
334
699
  }
package/sst-env.d.ts ADDED
@@ -0,0 +1,10 @@
1
+ /* This file is auto-generated by SST. Do not edit. */
2
+ /* tslint:disable */
3
+ /* eslint-disable */
4
+ /* deno-fmt-ignore-file */
5
+ /* biome-ignore-all lint: auto-generated */
6
+
7
+ /// <reference path="../../sst-env.d.ts" />
8
+
9
+ import 'sst';
10
+ export {};