instar 0.8.6 → 0.8.8

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.
@@ -0,0 +1,11 @@
1
+ > Why do I have a folder named ".vercel" in my project?
2
+ The ".vercel" folder is created when you link a directory to a Vercel project.
3
+
4
+ > What does the "project.json" file contain?
5
+ The "project.json" file contains:
6
+ - The ID of the Vercel project that you linked ("projectId")
7
+ - The ID of the user or team your Vercel project is owned by ("orgId")
8
+
9
+ > Should I commit the ".vercel" folder?
10
+ No, you should not share the ".vercel" folder with anyone.
11
+ Upon creation, it will be automatically added to your ".gitignore" file.
@@ -0,0 +1 @@
1
+ {"projectId":"prj_evM5LcItYL3IAmw8zNvEPGrHeaya","orgId":"team_dHctwIDcV3X9ydapQlCPHFGI","projectName":"claude-agent-kit"}
@@ -401,11 +401,149 @@
401
401
  margin-top: 8px;
402
402
  }
403
403
 
404
+ /* Mobile back button */
405
+ .back-btn {
406
+ display: none;
407
+ padding: 6px 12px;
408
+ border-radius: 6px;
409
+ border: 1px solid var(--border);
410
+ background: var(--bg);
411
+ color: var(--text);
412
+ font-size: 13px;
413
+ cursor: pointer;
414
+ margin-right: 8px;
415
+ }
416
+
417
+ .back-btn:hover { background: var(--bg-hover); }
418
+
404
419
  /* Scrollbar */
405
420
  ::-webkit-scrollbar { width: 6px; }
406
421
  ::-webkit-scrollbar-track { background: transparent; }
407
422
  ::-webkit-scrollbar-thumb { background: #333; border-radius: 3px; }
408
423
  ::-webkit-scrollbar-thumb:hover { background: #444; }
424
+
425
+ /* ── Mobile responsive ─────────────────────────────────── */
426
+ @media (max-width: 768px) {
427
+ .app {
428
+ grid-template-columns: 1fr;
429
+ }
430
+
431
+ .sidebar {
432
+ border-right: none;
433
+ border-bottom: 1px solid var(--border);
434
+ }
435
+
436
+ /* In mobile, sidebar and main toggle visibility */
437
+ .app.terminal-active .sidebar {
438
+ display: none;
439
+ }
440
+
441
+ .app:not(.terminal-active) .main {
442
+ display: none;
443
+ }
444
+
445
+ .app.terminal-active .back-btn {
446
+ display: inline-block;
447
+ }
448
+
449
+ /* Larger tap targets */
450
+ .session-item {
451
+ padding: 14px 12px;
452
+ margin-bottom: 4px;
453
+ }
454
+
455
+ .session-name {
456
+ font-size: 15px;
457
+ }
458
+
459
+ .session-meta {
460
+ font-size: 12px;
461
+ margin-top: 4px;
462
+ }
463
+
464
+ .action-btn {
465
+ padding: 8px 14px;
466
+ font-size: 14px;
467
+ }
468
+
469
+ .terminal-actions {
470
+ gap: 4px;
471
+ flex-wrap: wrap;
472
+ }
473
+
474
+ .terminal-header {
475
+ padding: 8px 12px;
476
+ flex-wrap: wrap;
477
+ gap: 8px;
478
+ }
479
+
480
+ .terminal-header .session-info {
481
+ flex: 1;
482
+ min-width: 0;
483
+ }
484
+
485
+ .terminal-header .session-info h3 {
486
+ font-size: 13px;
487
+ white-space: nowrap;
488
+ overflow: hidden;
489
+ text-overflow: ellipsis;
490
+ }
491
+
492
+ /* Input bar — stack on very small screens */
493
+ .input-bar {
494
+ padding: 8px;
495
+ gap: 6px;
496
+ }
497
+
498
+ .input-bar input {
499
+ font-size: 16px; /* Prevents iOS zoom on focus */
500
+ padding: 10px 12px;
501
+ }
502
+
503
+ .input-bar button {
504
+ padding: 10px 14px;
505
+ font-size: 14px;
506
+ }
507
+
508
+ /* Terminal font smaller on mobile */
509
+ .terminal-container {
510
+ padding: 2px;
511
+ }
512
+
513
+ /* Auth box responsive */
514
+ .auth-box {
515
+ width: calc(100vw - 32px);
516
+ max-width: 360px;
517
+ padding: 24px;
518
+ }
519
+
520
+ .auth-box input {
521
+ font-size: 16px; /* Prevents iOS zoom */
522
+ padding: 12px 14px;
523
+ }
524
+
525
+ .auth-box button {
526
+ padding: 12px;
527
+ font-size: 15px;
528
+ }
529
+
530
+ /* Header compact */
531
+ .header {
532
+ padding: 10px 14px;
533
+ }
534
+
535
+ .header h1 {
536
+ font-size: 15px;
537
+ }
538
+
539
+ .header .logo {
540
+ font-size: 18px;
541
+ }
542
+
543
+ .sidebar-header {
544
+ padding: 12px 14px;
545
+ }
546
+ }
409
547
  </style>
410
548
  </head>
411
549
  <body>
@@ -413,8 +551,8 @@
413
551
  <div class="auth-overlay" id="authOverlay">
414
552
  <div class="auth-box">
415
553
  <h2>Instar Dashboard</h2>
416
- <p>Enter your auth token to connect</p>
417
- <input type="password" id="tokenInput" placeholder="Bearer token..." autofocus>
554
+ <p>Enter your PIN to connect</p>
555
+ <input type="password" id="pinInput" placeholder="PIN..." autofocus inputmode="numeric">
418
556
  <button onclick="authenticate()">Connect</button>
419
557
  <div class="auth-error" id="authError" style="display:none"></div>
420
558
  </div>
@@ -455,6 +593,7 @@
455
593
  <div id="terminalView" style="display:none">
456
594
  <div class="terminal-header">
457
595
  <div class="session-info">
596
+ <button class="back-btn" onclick="goBack()" title="Back to sessions">&larr;</button>
458
597
  <h3 id="termSessionName">—</h3>
459
598
  <span class="model-badge" id="termModelBadge" style="display:none"></span>
460
599
  </div>
@@ -488,17 +627,35 @@
488
627
 
489
628
  // ── Auth ─────────────────────────────────────────────────
490
629
  function authenticate() {
491
- token = document.getElementById('tokenInput').value.trim();
492
- if (!token) {
493
- showAuthError('Please enter a token');
630
+ const pin = document.getElementById('pinInput').value.trim();
631
+ if (!pin) {
632
+ showAuthError('Please enter a PIN');
494
633
  return;
495
634
  }
496
635
 
497
- // Test token by hitting /health with auth
498
- const port = window.location.port || location.port;
499
- fetch(`/health`, {
500
- headers: { 'Authorization': `Bearer ${token}` }
636
+ // Try PIN-based unlock first, fall back to direct token auth
637
+ fetch('/dashboard/unlock', {
638
+ method: 'POST',
639
+ headers: { 'Content-Type': 'application/json' },
640
+ body: JSON.stringify({ pin }),
501
641
  })
642
+ .then(r => {
643
+ if (r.ok) return r.json();
644
+ if (r.status === 429) throw new Error('Too many attempts. Try again later.');
645
+ if (r.status === 403) return r.json().then(d => { throw new Error(d.error || 'Incorrect PIN'); });
646
+ // No PIN endpoint (404) — try as raw token
647
+ return null;
648
+ })
649
+ .then(data => {
650
+ if (data && data.token) {
651
+ token = data.token;
652
+ } else {
653
+ // Fall back: treat input as raw Bearer token
654
+ token = pin;
655
+ }
656
+ // Verify the token works
657
+ return fetch('/health', { headers: { 'Authorization': `Bearer ${token}` } });
658
+ })
502
659
  .then(r => {
503
660
  if (r.ok) {
504
661
  localStorage.setItem('instar_token', token);
@@ -506,10 +663,10 @@
506
663
  document.getElementById('app').style.display = 'grid';
507
664
  connectWebSocket();
508
665
  } else {
509
- showAuthError('Invalid token');
666
+ showAuthError('Incorrect PIN');
510
667
  }
511
668
  })
512
- .catch(() => showAuthError('Cannot connect to server'));
669
+ .catch(err => showAuthError(err.message || 'Cannot connect to server'));
513
670
  }
514
671
 
515
672
  function showAuthError(msg) {
@@ -522,7 +679,7 @@
522
679
  const stored = localStorage.getItem('instar_token');
523
680
  if (stored) {
524
681
  token = stored;
525
- fetch(`/health`, { headers: { 'Authorization': `Bearer ${token}` } })
682
+ fetch('/health', { headers: { 'Authorization': `Bearer ${token}` } })
526
683
  .then(r => {
527
684
  if (r.ok) {
528
685
  document.getElementById('authOverlay').style.display = 'none';
@@ -533,8 +690,8 @@
533
690
  .catch(() => {});
534
691
  }
535
692
 
536
- // Enter on token input
537
- document.getElementById('tokenInput').addEventListener('keydown', e => {
693
+ // Enter on PIN input
694
+ document.getElementById('pinInput').addEventListener('keydown', e => {
538
695
  if (e.key === 'Enter') authenticate();
539
696
  });
540
697
 
@@ -674,7 +831,7 @@
674
831
  }
675
832
 
676
833
  const elapsed = formatElapsed(session.startedAt);
677
- const model = session.model || 'sonnet';
834
+ const model = session.model || 'opus';
678
835
  const isActive = session.tmuxSession === activeSession;
679
836
 
680
837
  el.className = `session-item${isActive ? ' active' : ''}`;
@@ -698,6 +855,9 @@
698
855
 
699
856
  activeSession = tmuxSession;
700
857
 
858
+ // Mobile: show terminal, hide sidebar
859
+ document.getElementById('app').classList.add('terminal-active');
860
+
701
861
  // Update UI
702
862
  document.getElementById('noSession').style.display = 'none';
703
863
  document.getElementById('terminalView').style.display = 'flex';
@@ -757,7 +917,7 @@
757
917
  brightCyan: '#99f6e4',
758
918
  brightWhite: '#eee',
759
919
  },
760
- fontSize: 13,
920
+ fontSize: window.innerWidth <= 768 ? 11 : 13,
761
921
  fontFamily: "'SF Mono', 'Fira Code', 'JetBrains Mono', 'Cascadia Code', monospace",
762
922
  cursorBlink: false,
763
923
  cursorStyle: 'underline',
@@ -790,6 +950,18 @@
790
950
  term.scrollToBottom();
791
951
  }
792
952
 
953
+ function goBack() {
954
+ // Mobile: go back to session list
955
+ if (activeSession) {
956
+ wsSend({ type: 'unsubscribe', session: activeSession });
957
+ }
958
+ activeSession = null;
959
+ document.getElementById('app').classList.remove('terminal-active');
960
+ document.getElementById('terminalView').style.display = 'none';
961
+ document.getElementById('noSession').style.display = 'flex';
962
+ renderSessionList();
963
+ }
964
+
793
965
  // ── Input ────────────────────────────────────────────────
794
966
  function sendInput() {
795
967
  const input = document.getElementById('termInput');
package/dist/cli.js CHANGED
@@ -443,7 +443,7 @@ jobCmd
443
443
  .requiredOption('--schedule <cron>', 'Cron expression')
444
444
  .option('--description <desc>', 'Job description')
445
445
  .option('--priority <priority>', 'Priority (critical|high|medium|low)', 'medium')
446
- .option('--model <model>', 'Model tier (opus|sonnet|haiku)', 'sonnet')
446
+ .option('--model <model>', 'Model tier (opus|sonnet|haiku)', 'opus')
447
447
  .option('--type <type>', 'Execution type (skill|prompt|script)', 'prompt')
448
448
  .option('--execute <value>', 'Execution value (skill name, prompt text, or script path)')
449
449
  .action(addJob);
@@ -970,7 +970,7 @@ function getDefaultJobs(port) {
970
970
  schedule: '0 */4 * * *',
971
971
  priority: 'medium',
972
972
  expectedDurationMinutes: 5,
973
- model: 'sonnet',
973
+ model: 'opus',
974
974
  enabled: true,
975
975
  execute: {
976
976
  type: 'prompt',
@@ -985,7 +985,7 @@ function getDefaultJobs(port) {
985
985
  schedule: '0 9 * * *',
986
986
  priority: 'low',
987
987
  expectedDurationMinutes: 3,
988
- model: 'sonnet',
988
+ model: 'opus',
989
989
  enabled: true,
990
990
  execute: {
991
991
  type: 'prompt',
@@ -1048,7 +1048,7 @@ function getDefaultJobs(port) {
1048
1048
  schedule: '0 */2 * * *',
1049
1049
  priority: 'medium',
1050
1050
  expectedDurationMinutes: 3,
1051
- model: 'sonnet',
1051
+ model: 'opus',
1052
1052
  enabled: true,
1053
1053
  gate: `curl -sf http://localhost:${port}/health >/dev/null 2>&1`,
1054
1054
  execute: {
@@ -1088,7 +1088,7 @@ If everything looks healthy, exit silently. Only report issues.`,
1088
1088
  schedule: '0 */6 * * *',
1089
1089
  priority: 'medium',
1090
1090
  expectedDurationMinutes: 5,
1091
- model: 'sonnet',
1091
+ model: 'opus',
1092
1092
  enabled: true,
1093
1093
  gate: `curl -sf http://localhost:${port}/evolution/proposals?status=proposed 2>/dev/null | python3 -c "import sys,json; d=json.load(sys.stdin); exit(0 if len(d.get('proposals',[])) > 0 else 1)"`,
1094
1094
  execute: {
@@ -1117,7 +1117,7 @@ If no proposals need attention, exit silently.`,
1117
1117
  schedule: '0 */8 * * *',
1118
1118
  priority: 'low',
1119
1119
  expectedDurationMinutes: 3,
1120
- model: 'sonnet',
1120
+ model: 'opus',
1121
1121
  enabled: true,
1122
1122
  gate: `curl -sf http://localhost:${port}/evolution/learnings?applied=false 2>/dev/null | python3 -c "import sys,json; d=json.load(sys.stdin); exit(0 if len(d.get('learnings',[])) > 0 else 1)"`,
1123
1123
  execute: {
@@ -26,7 +26,7 @@ export async function addJob(options) {
26
26
  schedule: options.schedule,
27
27
  priority: (options.priority || 'medium'),
28
28
  expectedDurationMinutes: 5,
29
- model: (options.model || 'sonnet'),
29
+ model: (options.model || 'opus'),
30
30
  enabled: options.enabled !== false,
31
31
  execute: {
32
32
  type: (options.type || 'prompt'),
@@ -163,6 +163,7 @@ export function loadConfig(projectDir) {
163
163
  healthCheckIntervalMs: 30000,
164
164
  },
165
165
  authToken: fileConfig.authToken,
166
+ dashboardPin: fileConfig.dashboardPin,
166
167
  relationships: fileConfig.relationships || {
167
168
  relationshipsDir: path.join(stateDir, 'relationships'),
168
169
  maxRecentInteractions: 20,
@@ -615,6 +615,8 @@ export interface InstarConfig {
615
615
  monitoring: MonitoringConfig;
616
616
  /** Auth token for API access (generated during setup) */
617
617
  authToken?: string;
618
+ /** PIN for dashboard web access (simpler than authToken, used for mobile/remote login) */
619
+ dashboardPin?: string;
618
620
  /** Relationship tracking config */
619
621
  relationships?: RelationshipManagerConfig;
620
622
  /** Feedback loop config */
@@ -10,6 +10,7 @@
10
10
  import express from 'express';
11
11
  import fs from 'node:fs';
12
12
  import path from 'node:path';
13
+ import { createHash, timingSafeEqual } from 'node:crypto';
13
14
  import { fileURLToPath } from 'node:url';
14
15
  import { createRoutes } from './routes.js';
15
16
  import { corsMiddleware, authMiddleware, requestTimeout, errorHandler } from './middleware.js';
@@ -38,6 +39,50 @@ export class AgentServer {
38
39
  res.sendFile(path.join(dashboardDir, 'index.html'));
39
40
  });
40
41
  this.app.use('/dashboard', express.static(dashboardDir));
42
+ // PIN-based dashboard unlock — exchanges a short PIN for the auth token.
43
+ // Placed before auth middleware so the dashboard can call it without a token.
44
+ if (options.config.dashboardPin && options.config.authToken) {
45
+ const pinAttempts = new Map();
46
+ const MAX_ATTEMPTS = 5;
47
+ const WINDOW_MS = 5 * 60 * 1000; // 5-minute window
48
+ this.app.post('/dashboard/unlock', (req, res) => {
49
+ const ip = req.ip || req.socket.remoteAddress || 'unknown';
50
+ // Rate limit by IP
51
+ const now = Date.now();
52
+ let entry = pinAttempts.get(ip);
53
+ if (entry && now > entry.resetAt) {
54
+ pinAttempts.delete(ip);
55
+ entry = undefined;
56
+ }
57
+ if (entry && entry.count >= MAX_ATTEMPTS) {
58
+ res.status(429).json({ error: 'Too many attempts. Try again later.' });
59
+ return;
60
+ }
61
+ const { pin } = req.body;
62
+ if (!pin || typeof pin !== 'string') {
63
+ res.status(400).json({ error: 'Missing PIN' });
64
+ return;
65
+ }
66
+ const ha = createHash('sha256').update(pin).digest();
67
+ const hb = createHash('sha256').update(options.config.dashboardPin).digest();
68
+ if (!timingSafeEqual(ha, hb)) {
69
+ // Track failed attempt
70
+ if (!entry) {
71
+ entry = { count: 0, resetAt: now + WINDOW_MS };
72
+ pinAttempts.set(ip, entry);
73
+ }
74
+ entry.count++;
75
+ const remaining = MAX_ATTEMPTS - entry.count;
76
+ res.status(403).json({
77
+ error: 'Incorrect PIN',
78
+ attemptsRemaining: remaining,
79
+ });
80
+ return;
81
+ }
82
+ // PIN correct — return the auth token
83
+ res.json({ token: options.config.authToken });
84
+ });
85
+ }
41
86
  this.app.use(authMiddleware(options.config.authToken));
42
87
  this.app.use(requestTimeout(options.config.requestTimeoutMs));
43
88
  // Routes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "instar",
3
- "version": "0.8.6",
3
+ "version": "0.8.8",
4
4
  "description": "Persistent autonomy infrastructure for AI agents",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",