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.
- package/.vercel/README.txt +11 -0
- package/.vercel/project.json +1 -0
- package/dashboard/index.html +188 -16
- package/dist/cli.js +1 -1
- package/dist/commands/init.js +5 -5
- package/dist/commands/job.js +1 -1
- package/dist/core/Config.js +1 -0
- package/dist/core/types.d.ts +2 -0
- package/dist/server/AgentServer.js +45 -0
- package/package.json +1 -1
|
@@ -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"}
|
package/dashboard/index.html
CHANGED
|
@@ -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
|
|
417
|
-
<input type="password" id="
|
|
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">←</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
|
-
|
|
492
|
-
if (!
|
|
493
|
-
showAuthError('Please enter a
|
|
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
|
-
//
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
headers: { '
|
|
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('
|
|
666
|
+
showAuthError('Incorrect PIN');
|
|
510
667
|
}
|
|
511
668
|
})
|
|
512
|
-
.catch(
|
|
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(
|
|
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
|
|
537
|
-
document.getElementById('
|
|
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 || '
|
|
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)', '
|
|
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);
|
package/dist/commands/init.js
CHANGED
|
@@ -970,7 +970,7 @@ function getDefaultJobs(port) {
|
|
|
970
970
|
schedule: '0 */4 * * *',
|
|
971
971
|
priority: 'medium',
|
|
972
972
|
expectedDurationMinutes: 5,
|
|
973
|
-
model: '
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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: {
|
package/dist/commands/job.js
CHANGED
|
@@ -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 || '
|
|
29
|
+
model: (options.model || 'opus'),
|
|
30
30
|
enabled: options.enabled !== false,
|
|
31
31
|
execute: {
|
|
32
32
|
type: (options.type || 'prompt'),
|
package/dist/core/Config.js
CHANGED
|
@@ -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,
|
package/dist/core/types.d.ts
CHANGED
|
@@ -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
|