instar 0.1.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/.claude/settings.local.json +7 -0
- package/.claude/skills/setup-wizard/skill.md +343 -0
- package/.github/workflows/ci.yml +78 -0
- package/CLAUDE.md +82 -0
- package/README.md +194 -0
- package/dist/cli.d.ts +18 -0
- package/dist/cli.js +141 -0
- package/dist/commands/init.d.ts +40 -0
- package/dist/commands/init.js +568 -0
- package/dist/commands/job.d.ts +20 -0
- package/dist/commands/job.js +84 -0
- package/dist/commands/server.d.ts +19 -0
- package/dist/commands/server.js +273 -0
- package/dist/commands/setup.d.ts +24 -0
- package/dist/commands/setup.js +865 -0
- package/dist/commands/status.d.ts +11 -0
- package/dist/commands/status.js +114 -0
- package/dist/commands/user.d.ts +17 -0
- package/dist/commands/user.js +53 -0
- package/dist/core/Config.d.ts +16 -0
- package/dist/core/Config.js +144 -0
- package/dist/core/Prerequisites.d.ts +28 -0
- package/dist/core/Prerequisites.js +159 -0
- package/dist/core/RelationshipManager.d.ts +73 -0
- package/dist/core/RelationshipManager.js +318 -0
- package/dist/core/SessionManager.d.ts +89 -0
- package/dist/core/SessionManager.js +326 -0
- package/dist/core/StateManager.d.ts +28 -0
- package/dist/core/StateManager.js +96 -0
- package/dist/core/types.d.ts +279 -0
- package/dist/core/types.js +8 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.js +23 -0
- package/dist/messaging/TelegramAdapter.d.ts +73 -0
- package/dist/messaging/TelegramAdapter.js +288 -0
- package/dist/monitoring/HealthChecker.d.ts +38 -0
- package/dist/monitoring/HealthChecker.js +148 -0
- package/dist/scaffold/bootstrap.d.ts +21 -0
- package/dist/scaffold/bootstrap.js +110 -0
- package/dist/scaffold/templates.d.ts +34 -0
- package/dist/scaffold/templates.js +187 -0
- package/dist/scheduler/JobLoader.d.ts +18 -0
- package/dist/scheduler/JobLoader.js +70 -0
- package/dist/scheduler/JobScheduler.d.ts +111 -0
- package/dist/scheduler/JobScheduler.js +402 -0
- package/dist/server/AgentServer.d.ts +40 -0
- package/dist/server/AgentServer.js +73 -0
- package/dist/server/middleware.d.ts +12 -0
- package/dist/server/middleware.js +50 -0
- package/dist/server/routes.d.ts +25 -0
- package/dist/server/routes.js +224 -0
- package/dist/users/UserManager.d.ts +45 -0
- package/dist/users/UserManager.js +113 -0
- package/docs/dawn-audit-report.md +412 -0
- package/docs/positioning-vs-openclaw.md +246 -0
- package/package.json +52 -0
- package/src/cli.ts +169 -0
- package/src/commands/init.ts +654 -0
- package/src/commands/job.ts +110 -0
- package/src/commands/server.ts +325 -0
- package/src/commands/setup.ts +958 -0
- package/src/commands/status.ts +125 -0
- package/src/commands/user.ts +71 -0
- package/src/core/Config.ts +161 -0
- package/src/core/Prerequisites.ts +187 -0
- package/src/core/RelationshipManager.ts +366 -0
- package/src/core/SessionManager.ts +385 -0
- package/src/core/StateManager.ts +121 -0
- package/src/core/types.ts +320 -0
- package/src/index.ts +58 -0
- package/src/messaging/TelegramAdapter.ts +365 -0
- package/src/monitoring/HealthChecker.ts +172 -0
- package/src/scaffold/bootstrap.ts +122 -0
- package/src/scaffold/templates.ts +204 -0
- package/src/scheduler/JobLoader.ts +85 -0
- package/src/scheduler/JobScheduler.ts +476 -0
- package/src/server/AgentServer.ts +93 -0
- package/src/server/middleware.ts +58 -0
- package/src/server/routes.ts +278 -0
- package/src/templates/default-jobs.json +47 -0
- package/src/templates/hooks/compaction-recovery.sh +23 -0
- package/src/templates/hooks/dangerous-command-guard.sh +35 -0
- package/src/templates/hooks/grounding-before-messaging.sh +22 -0
- package/src/templates/hooks/session-start.sh +37 -0
- package/src/templates/hooks/settings-template.json +45 -0
- package/src/templates/scripts/health-watchdog.sh +63 -0
- package/src/templates/scripts/telegram-reply.sh +54 -0
- package/src/users/UserManager.ts +129 -0
- package/tests/e2e/lifecycle.test.ts +376 -0
- package/tests/fixtures/test-repo/CLAUDE.md +3 -0
- package/tests/fixtures/test-repo/README.md +1 -0
- package/tests/helpers/setup.ts +209 -0
- package/tests/integration/fresh-install.test.ts +218 -0
- package/tests/integration/scheduler-basic.test.ts +109 -0
- package/tests/integration/server-full.test.ts +284 -0
- package/tests/integration/session-lifecycle.test.ts +181 -0
- package/tests/unit/Config.test.ts +22 -0
- package/tests/unit/HealthChecker.test.ts +168 -0
- package/tests/unit/JobLoader.test.ts +151 -0
- package/tests/unit/JobScheduler.test.ts +267 -0
- package/tests/unit/Prerequisites.test.ts +59 -0
- package/tests/unit/RelationshipManager.test.ts +345 -0
- package/tests/unit/StateManager.test.ts +143 -0
- package/tests/unit/TelegramAdapter.test.ts +165 -0
- package/tests/unit/UserManager.test.ts +131 -0
- package/tests/unit/bootstrap.test.ts +28 -0
- package/tests/unit/commands.test.ts +138 -0
- package/tests/unit/middleware.test.ts +92 -0
- package/tests/unit/relationship-routes.test.ts +131 -0
- package/tests/unit/scaffold-templates.test.ts +132 -0
- package/tests/unit/server.test.ts +163 -0
- package/tsconfig.json +20 -0
- package/vitest.config.ts +9 -0
- package/vitest.e2e.config.ts +9 -0
- package/vitest.integration.config.ts +9 -0
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTTP API routes — health, status, sessions, jobs, events.
|
|
3
|
+
*
|
|
4
|
+
* Extracted/simplified from Dawn's 2267-line routes.ts.
|
|
5
|
+
* All the observability you need, none of the complexity you don't.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { Router } from 'express';
|
|
9
|
+
import { execSync as execSyncFn } from 'node:child_process';
|
|
10
|
+
import type { SessionManager } from '../core/SessionManager.js';
|
|
11
|
+
import type { StateManager } from '../core/StateManager.js';
|
|
12
|
+
import type { JobScheduler } from '../scheduler/JobScheduler.js';
|
|
13
|
+
import type { AgentKitConfig } from '../core/types.js';
|
|
14
|
+
import type { TelegramAdapter } from '../messaging/TelegramAdapter.js';
|
|
15
|
+
import type { RelationshipManager } from '../core/RelationshipManager.js';
|
|
16
|
+
|
|
17
|
+
interface RouteContext {
|
|
18
|
+
config: AgentKitConfig;
|
|
19
|
+
sessionManager: SessionManager;
|
|
20
|
+
state: StateManager;
|
|
21
|
+
scheduler: JobScheduler | null;
|
|
22
|
+
telegram: TelegramAdapter | null;
|
|
23
|
+
relationships: RelationshipManager | null;
|
|
24
|
+
startTime: Date;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function createRoutes(ctx: RouteContext): Router {
|
|
28
|
+
const router = Router();
|
|
29
|
+
|
|
30
|
+
// ── Health ──────────────────────────────────────────────────────
|
|
31
|
+
|
|
32
|
+
router.get('/health', (_req, res) => {
|
|
33
|
+
const uptimeMs = Date.now() - ctx.startTime.getTime();
|
|
34
|
+
res.json({
|
|
35
|
+
status: 'ok',
|
|
36
|
+
uptime: uptimeMs,
|
|
37
|
+
uptimeHuman: formatUptime(uptimeMs),
|
|
38
|
+
version: '0.1.0',
|
|
39
|
+
project: ctx.config.projectName,
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
// ── Status ──────────────────────────────────────────────────────
|
|
44
|
+
|
|
45
|
+
router.get('/status', (_req, res) => {
|
|
46
|
+
const sessions = ctx.sessionManager.listRunningSessions();
|
|
47
|
+
const schedulerStatus = ctx.scheduler?.getStatus() ?? null;
|
|
48
|
+
|
|
49
|
+
res.json({
|
|
50
|
+
sessions: {
|
|
51
|
+
running: sessions.length,
|
|
52
|
+
max: ctx.config.sessions.maxSessions,
|
|
53
|
+
list: sessions.map(s => ({ id: s.id, name: s.name, jobSlug: s.jobSlug })),
|
|
54
|
+
},
|
|
55
|
+
scheduler: schedulerStatus,
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
// ── Sessions ────────────────────────────────────────────────────
|
|
60
|
+
|
|
61
|
+
router.get('/sessions', (req, res) => {
|
|
62
|
+
const status = req.query.status as string | undefined;
|
|
63
|
+
const sessions = status
|
|
64
|
+
? ctx.state.listSessions({ status: status as any })
|
|
65
|
+
: ctx.state.listSessions();
|
|
66
|
+
|
|
67
|
+
res.json(sessions);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
router.get('/sessions/:name/output', (req, res) => {
|
|
71
|
+
const lines = parseInt(req.query.lines as string) || 100;
|
|
72
|
+
const output = ctx.sessionManager.captureOutput(req.params.name, lines);
|
|
73
|
+
|
|
74
|
+
if (output === null) {
|
|
75
|
+
res.status(404).json({ error: `Session "${req.params.name}" not found or not running` });
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
res.json({ session: req.params.name, output });
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
router.post('/sessions/:name/input', (req, res) => {
|
|
83
|
+
const { text } = req.body;
|
|
84
|
+
if (!text || typeof text !== 'string') {
|
|
85
|
+
res.status(400).json({ error: 'Request body must include "text" field' });
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const success = ctx.sessionManager.sendInput(req.params.name, text);
|
|
90
|
+
if (!success) {
|
|
91
|
+
res.status(404).json({ error: `Session "${req.params.name}" not found or not running` });
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
res.json({ ok: true });
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
router.post('/sessions/spawn', (req, res) => {
|
|
99
|
+
const { name, prompt, model, jobSlug } = req.body;
|
|
100
|
+
|
|
101
|
+
if (!name || !prompt) {
|
|
102
|
+
res.status(400).json({ error: '"name" and "prompt" are required' });
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
try {
|
|
107
|
+
const session = ctx.sessionManager.spawnSession({ name, prompt, model, jobSlug });
|
|
108
|
+
// spawnSession is async but we want to handle errors,
|
|
109
|
+
// so we use .then/.catch
|
|
110
|
+
session.then(s => res.status(201).json(s)).catch(err => {
|
|
111
|
+
res.status(500).json({ error: err.message });
|
|
112
|
+
});
|
|
113
|
+
} catch (err: any) {
|
|
114
|
+
res.status(500).json({ error: err.message });
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
router.delete('/sessions/:id', (req, res) => {
|
|
119
|
+
try {
|
|
120
|
+
const killed = ctx.sessionManager.killSession(req.params.id);
|
|
121
|
+
if (!killed) {
|
|
122
|
+
res.status(404).json({ error: `Session "${req.params.id}" not found` });
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
res.json({ ok: true, killed: req.params.id });
|
|
126
|
+
} catch (err: any) {
|
|
127
|
+
res.status(400).json({ error: err.message });
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
// ── Jobs ────────────────────────────────────────────────────────
|
|
132
|
+
|
|
133
|
+
router.get('/jobs', (_req, res) => {
|
|
134
|
+
if (!ctx.scheduler) {
|
|
135
|
+
res.json({ jobs: [], scheduler: null });
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const jobs = ctx.scheduler.getJobs().map(job => {
|
|
140
|
+
const jobState = ctx.state.getJobState(job.slug);
|
|
141
|
+
return { ...job, state: jobState };
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
res.json({ jobs, queue: ctx.scheduler.getQueue() });
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
router.post('/jobs/:slug/trigger', (req, res) => {
|
|
148
|
+
if (!ctx.scheduler) {
|
|
149
|
+
res.status(503).json({ error: 'Scheduler not running' });
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const reason = (req.body?.reason as string) || 'manual';
|
|
154
|
+
|
|
155
|
+
try {
|
|
156
|
+
const result = ctx.scheduler.triggerJob(req.params.slug, reason);
|
|
157
|
+
res.json({ slug: req.params.slug, result });
|
|
158
|
+
} catch (err: any) {
|
|
159
|
+
res.status(404).json({ error: err.message });
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
// ── Telegram ────────────────────────────────────────────────────
|
|
164
|
+
|
|
165
|
+
router.post('/telegram/reply/:topicId', async (req, res) => {
|
|
166
|
+
if (!ctx.telegram) {
|
|
167
|
+
res.status(503).json({ error: 'Telegram not configured' });
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const topicId = parseInt(req.params.topicId);
|
|
172
|
+
const { text } = req.body;
|
|
173
|
+
if (!text || typeof text !== 'string') {
|
|
174
|
+
res.status(400).json({ error: '"text" field required' });
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
try {
|
|
179
|
+
await ctx.telegram.sendToTopic(topicId, text);
|
|
180
|
+
res.json({ ok: true, topicId });
|
|
181
|
+
} catch (err: any) {
|
|
182
|
+
res.status(500).json({ error: err.message });
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
// ── tmux Sessions (raw) ─────────────────────────────────────────
|
|
187
|
+
|
|
188
|
+
router.get('/sessions/tmux', (_req, res) => {
|
|
189
|
+
try {
|
|
190
|
+
const tmuxPath = ctx.config.sessions.tmuxPath;
|
|
191
|
+
const output = execSyncFn(`${tmuxPath} list-sessions -F '#{session_name}' 2>/dev/null || true`, {
|
|
192
|
+
encoding: 'utf-8',
|
|
193
|
+
}).trim();
|
|
194
|
+
|
|
195
|
+
const sessions = output
|
|
196
|
+
? output.split('\n').filter(Boolean).map((name: string) => ({ name }))
|
|
197
|
+
: [];
|
|
198
|
+
|
|
199
|
+
res.json({ sessions });
|
|
200
|
+
} catch {
|
|
201
|
+
res.json({ sessions: [] });
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
// ── Relationships ─────────────────────────────────────────────────
|
|
206
|
+
|
|
207
|
+
router.get('/relationships', (_req, res) => {
|
|
208
|
+
if (!ctx.relationships) {
|
|
209
|
+
res.json({ relationships: [] });
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
const sortBy = (_req.query.sort as 'significance' | 'recent' | 'name') || 'significance';
|
|
213
|
+
res.json({ relationships: ctx.relationships.getAll(sortBy) });
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
// Stale must be before :id to avoid "stale" matching as a param
|
|
217
|
+
router.get('/relationships/stale', (req, res) => {
|
|
218
|
+
if (!ctx.relationships) {
|
|
219
|
+
res.json({ stale: [] });
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
const days = parseInt(req.query.days as string) || 14;
|
|
223
|
+
res.json({ stale: ctx.relationships.getStaleRelationships(days) });
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
router.get('/relationships/:id', (req, res) => {
|
|
227
|
+
if (!ctx.relationships) {
|
|
228
|
+
res.status(503).json({ error: 'Relationships not configured' });
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
const record = ctx.relationships.get(req.params.id);
|
|
232
|
+
if (!record) {
|
|
233
|
+
res.status(404).json({ error: 'Relationship not found' });
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
res.json(record);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
router.get('/relationships/:id/context', (req, res) => {
|
|
240
|
+
if (!ctx.relationships) {
|
|
241
|
+
res.status(503).json({ error: 'Relationships not configured' });
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
const context = ctx.relationships.getContextForPerson(req.params.id);
|
|
245
|
+
if (!context) {
|
|
246
|
+
res.status(404).json({ error: 'Relationship not found' });
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
res.json({ context });
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
// ── Events ──────────────────────────────────────────────────────
|
|
253
|
+
|
|
254
|
+
router.get('/events', (req, res) => {
|
|
255
|
+
const limit = parseInt(req.query.limit as string) || 50;
|
|
256
|
+
const type = req.query.type as string | undefined;
|
|
257
|
+
const sinceHours = parseInt(req.query.since as string) || 24;
|
|
258
|
+
|
|
259
|
+
const since = new Date(Date.now() - sinceHours * 60 * 60 * 1000);
|
|
260
|
+
const events = ctx.state.queryEvents({ since, type, limit });
|
|
261
|
+
|
|
262
|
+
res.json(events);
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
return router;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function formatUptime(ms: number): string {
|
|
269
|
+
const seconds = Math.floor(ms / 1000);
|
|
270
|
+
const minutes = Math.floor(seconds / 60);
|
|
271
|
+
const hours = Math.floor(minutes / 60);
|
|
272
|
+
const days = Math.floor(hours / 24);
|
|
273
|
+
|
|
274
|
+
if (days > 0) return `${days}d ${hours % 24}h`;
|
|
275
|
+
if (hours > 0) return `${hours}h ${minutes % 60}m`;
|
|
276
|
+
if (minutes > 0) return `${minutes}m ${seconds % 60}s`;
|
|
277
|
+
return `${seconds}s`;
|
|
278
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"slug": "health-check",
|
|
4
|
+
"name": "Health Check",
|
|
5
|
+
"description": "Monitor server health, session status, and system resources. Alert if anything needs attention.",
|
|
6
|
+
"schedule": "*/5 * * * *",
|
|
7
|
+
"priority": "critical",
|
|
8
|
+
"expectedDurationMinutes": 1,
|
|
9
|
+
"model": "haiku",
|
|
10
|
+
"enabled": true,
|
|
11
|
+
"execute": {
|
|
12
|
+
"type": "prompt",
|
|
13
|
+
"value": "Run a quick health check: verify the instar server is responding (curl http://localhost:PORT/health), check disk space (df -h), and report any issues to the Agent Attention topic via telegram-reply.sh. Only send a message if something needs attention — silence means healthy."
|
|
14
|
+
},
|
|
15
|
+
"tags": ["coherence", "default"]
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
"slug": "reflection-trigger",
|
|
19
|
+
"name": "Reflection Trigger",
|
|
20
|
+
"description": "Review recent work and update MEMORY.md if any learnings exist. Ensures growth is captured, not lost.",
|
|
21
|
+
"schedule": "0 */4 * * *",
|
|
22
|
+
"priority": "medium",
|
|
23
|
+
"expectedDurationMinutes": 5,
|
|
24
|
+
"model": "sonnet",
|
|
25
|
+
"enabled": true,
|
|
26
|
+
"execute": {
|
|
27
|
+
"type": "prompt",
|
|
28
|
+
"value": "Review what has happened in the last 4 hours by reading recent activity logs and session reports. If there are any learnings, patterns, or insights worth remembering, update .instar/MEMORY.md. If nothing significant happened, do nothing. This is about capturing growth — not generating busy work."
|
|
29
|
+
},
|
|
30
|
+
"tags": ["coherence", "default"]
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"slug": "relationship-maintenance",
|
|
34
|
+
"name": "Relationship Maintenance",
|
|
35
|
+
"description": "Review tracked relationships. Surface observations about stale contacts or notable patterns.",
|
|
36
|
+
"schedule": "0 9 * * *",
|
|
37
|
+
"priority": "low",
|
|
38
|
+
"expectedDurationMinutes": 3,
|
|
39
|
+
"model": "sonnet",
|
|
40
|
+
"enabled": true,
|
|
41
|
+
"execute": {
|
|
42
|
+
"type": "prompt",
|
|
43
|
+
"value": "Review all relationship files in .instar/relationships/. Note anyone you haven't heard from in over 2 weeks who has significance >= 3. If there are observations worth surfacing (stale important relationships, patterns across conversations, things people mentioned that might need follow-up), send a brief summary to the Agent Attention topic. If everything looks fine, do nothing."
|
|
44
|
+
},
|
|
45
|
+
"tags": ["coherence", "default"]
|
|
46
|
+
}
|
|
47
|
+
]
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Compaction recovery — re-injects identity when Claude's context compresses.
|
|
3
|
+
# Without this, the agent loses its identity every 30-60 minutes.
|
|
4
|
+
#
|
|
5
|
+
# This is the single most impactful hook for agent continuity.
|
|
6
|
+
# When context compresses, Claude effectively starts over. This hook
|
|
7
|
+
# ensures the agent knows who it is after every compaction event.
|
|
8
|
+
#
|
|
9
|
+
# Installed by instar during setup. Runs as a Claude Code PostToolUse hook.
|
|
10
|
+
|
|
11
|
+
INSTAR_DIR="${CLAUDE_PROJECT_DIR:-.}/.instar"
|
|
12
|
+
|
|
13
|
+
# Check if we're in a post-compaction state by looking for compaction markers
|
|
14
|
+
# Claude Code emits specific patterns when context is compressed
|
|
15
|
+
# This hook provides the recovery seed
|
|
16
|
+
|
|
17
|
+
if [ -f "$INSTAR_DIR/AGENT.md" ]; then
|
|
18
|
+
AGENT_NAME=$(head -5 "$INSTAR_DIR/AGENT.md" | grep -i "name\|I am\|My name" | head -1)
|
|
19
|
+
if [ -n "$AGENT_NAME" ]; then
|
|
20
|
+
echo "Identity reminder: $AGENT_NAME"
|
|
21
|
+
echo "Read .instar/AGENT.md and .instar/MEMORY.md to restore full context."
|
|
22
|
+
fi
|
|
23
|
+
fi
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Dangerous command guard — blocks destructive operations.
|
|
3
|
+
# Part of instar's "Security Through Identity" model.
|
|
4
|
+
#
|
|
5
|
+
# Installed by instar during setup. Runs as a Claude Code PreToolUse hook on Bash.
|
|
6
|
+
|
|
7
|
+
# The command being executed is passed via TOOL_INPUT
|
|
8
|
+
INPUT="$1"
|
|
9
|
+
|
|
10
|
+
# Patterns that should be blocked without explicit user confirmation
|
|
11
|
+
DANGEROUS_PATTERNS=(
|
|
12
|
+
"rm -rf /"
|
|
13
|
+
"rm -rf ~"
|
|
14
|
+
"rm -rf \."
|
|
15
|
+
"git push --force"
|
|
16
|
+
"git push -f"
|
|
17
|
+
"git reset --hard"
|
|
18
|
+
"git clean -fd"
|
|
19
|
+
"DROP TABLE"
|
|
20
|
+
"DROP DATABASE"
|
|
21
|
+
"TRUNCATE"
|
|
22
|
+
"DELETE FROM"
|
|
23
|
+
"> /dev/sda"
|
|
24
|
+
"mkfs\."
|
|
25
|
+
"dd if="
|
|
26
|
+
":(){:|:&};:"
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
for pattern in "${DANGEROUS_PATTERNS[@]}"; do
|
|
30
|
+
if echo "$INPUT" | grep -qi "$pattern"; then
|
|
31
|
+
echo "BLOCKED: Potentially destructive command detected: $pattern"
|
|
32
|
+
echo "If you genuinely need to run this command, ask the user for explicit confirmation first."
|
|
33
|
+
exit 2
|
|
34
|
+
fi
|
|
35
|
+
done
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Grounding before messaging — ensures the agent re-reads its identity
|
|
3
|
+
# before sending any external message. Part of "Security Through Identity."
|
|
4
|
+
#
|
|
5
|
+
# This is both behavioral integrity AND security:
|
|
6
|
+
# - An agent that knows who it is can detect "this doesn't sound like me"
|
|
7
|
+
# - Identity grounding acts as an immune system against prompt injection
|
|
8
|
+
#
|
|
9
|
+
# Installed by instar during setup. Runs as a Claude Code PreToolUse hook on Bash.
|
|
10
|
+
|
|
11
|
+
INPUT="$1"
|
|
12
|
+
|
|
13
|
+
# Detect messaging commands (telegram-reply, email sends, etc.)
|
|
14
|
+
if echo "$INPUT" | grep -qE "(telegram-reply|send-email|send-message|POST.*/telegram/reply)"; then
|
|
15
|
+
INSTAR_DIR="${CLAUDE_PROJECT_DIR:-.}/.instar"
|
|
16
|
+
|
|
17
|
+
if [ -f "$INSTAR_DIR/AGENT.md" ]; then
|
|
18
|
+
echo "Before sending this message, remember who you are."
|
|
19
|
+
echo "Re-read .instar/AGENT.md if you haven't recently."
|
|
20
|
+
echo "Security Through Identity: An agent that knows itself is harder to compromise."
|
|
21
|
+
fi
|
|
22
|
+
fi
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Session start hook — injects identity context when a new Claude session begins.
|
|
3
|
+
# This is how the agent maintains continuity: every session starts with self-knowledge.
|
|
4
|
+
#
|
|
5
|
+
# Installed by instar during setup. Runs as a Claude Code PreToolUse hook.
|
|
6
|
+
|
|
7
|
+
INSTAR_DIR="${CLAUDE_PROJECT_DIR:-.}/.instar"
|
|
8
|
+
|
|
9
|
+
# Build identity context
|
|
10
|
+
CONTEXT=""
|
|
11
|
+
|
|
12
|
+
# Core identity
|
|
13
|
+
if [ -f "$INSTAR_DIR/AGENT.md" ]; then
|
|
14
|
+
CONTEXT="${CONTEXT}Your identity file is at .instar/AGENT.md — read it if you need to remember who you are.\n"
|
|
15
|
+
fi
|
|
16
|
+
|
|
17
|
+
# User context
|
|
18
|
+
if [ -f "$INSTAR_DIR/USER.md" ]; then
|
|
19
|
+
CONTEXT="${CONTEXT}Your user context is at .instar/USER.md — read it to know who you're working with.\n"
|
|
20
|
+
fi
|
|
21
|
+
|
|
22
|
+
# Memory
|
|
23
|
+
if [ -f "$INSTAR_DIR/MEMORY.md" ]; then
|
|
24
|
+
CONTEXT="${CONTEXT}Your persistent memory is at .instar/MEMORY.md — check it for past learnings.\n"
|
|
25
|
+
fi
|
|
26
|
+
|
|
27
|
+
# Relationships
|
|
28
|
+
if [ -d "$INSTAR_DIR/relationships" ]; then
|
|
29
|
+
REL_COUNT=$(ls -1 "$INSTAR_DIR/relationships"/*.json 2>/dev/null | wc -l | tr -d ' ')
|
|
30
|
+
if [ "$REL_COUNT" -gt "0" ]; then
|
|
31
|
+
CONTEXT="${CONTEXT}You have ${REL_COUNT} tracked relationships in .instar/relationships/.\n"
|
|
32
|
+
fi
|
|
33
|
+
fi
|
|
34
|
+
|
|
35
|
+
if [ -n "$CONTEXT" ]; then
|
|
36
|
+
echo "$CONTEXT"
|
|
37
|
+
fi
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"hooks": {
|
|
3
|
+
"PreToolUse": [
|
|
4
|
+
{
|
|
5
|
+
"matcher": "Bash",
|
|
6
|
+
"hooks": [
|
|
7
|
+
{
|
|
8
|
+
"type": "command",
|
|
9
|
+
"command": "bash .instar/hooks/dangerous-command-guard.sh \"$TOOL_INPUT\"",
|
|
10
|
+
"blocking": true
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
"type": "command",
|
|
14
|
+
"command": "bash .instar/hooks/grounding-before-messaging.sh \"$TOOL_INPUT\"",
|
|
15
|
+
"blocking": false
|
|
16
|
+
}
|
|
17
|
+
]
|
|
18
|
+
}
|
|
19
|
+
],
|
|
20
|
+
"PostToolUse": [
|
|
21
|
+
{
|
|
22
|
+
"matcher": "",
|
|
23
|
+
"hooks": [
|
|
24
|
+
{
|
|
25
|
+
"type": "command",
|
|
26
|
+
"command": "bash .instar/hooks/session-start.sh",
|
|
27
|
+
"blocking": false
|
|
28
|
+
}
|
|
29
|
+
]
|
|
30
|
+
}
|
|
31
|
+
],
|
|
32
|
+
"Notification": [
|
|
33
|
+
{
|
|
34
|
+
"matcher": "compaction",
|
|
35
|
+
"hooks": [
|
|
36
|
+
{
|
|
37
|
+
"type": "command",
|
|
38
|
+
"command": "bash .instar/hooks/compaction-recovery.sh",
|
|
39
|
+
"blocking": false
|
|
40
|
+
}
|
|
41
|
+
]
|
|
42
|
+
}
|
|
43
|
+
]
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# health-watchdog.sh — Monitor instar server and auto-recover.
|
|
3
|
+
#
|
|
4
|
+
# Install as a cron job:
|
|
5
|
+
# */5 * * * * /path/to/health-watchdog.sh >> /path/to/.instar/logs/watchdog.log 2>&1
|
|
6
|
+
#
|
|
7
|
+
# Or run via launchd on macOS.
|
|
8
|
+
|
|
9
|
+
# Configuration — set these for your project
|
|
10
|
+
PROJECT_DIR="${INSTAR_PROJECT_DIR:-$(dirname "$(dirname "$(realpath "$0")")")}"
|
|
11
|
+
PORT="${INSTAR_PORT:-4040}"
|
|
12
|
+
SERVER_SESSION="${INSTAR_SERVER_SESSION:-agent-server}"
|
|
13
|
+
TMUX_PATH="${INSTAR_TMUX:-/opt/homebrew/bin/tmux}"
|
|
14
|
+
|
|
15
|
+
# Find tmux if not at default path
|
|
16
|
+
if [ ! -f "$TMUX_PATH" ]; then
|
|
17
|
+
TMUX_PATH=$(which tmux 2>/dev/null)
|
|
18
|
+
fi
|
|
19
|
+
|
|
20
|
+
if [ -z "$TMUX_PATH" ] || [ ! -f "$TMUX_PATH" ]; then
|
|
21
|
+
echo "[$(date -Iseconds)] ERROR: tmux not found"
|
|
22
|
+
exit 1
|
|
23
|
+
fi
|
|
24
|
+
|
|
25
|
+
# Check if server is responding
|
|
26
|
+
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" "http://localhost:${PORT}/health" 2>/dev/null)
|
|
27
|
+
|
|
28
|
+
if [ "$HTTP_CODE" = "200" ]; then
|
|
29
|
+
# Server is healthy — nothing to do
|
|
30
|
+
exit 0
|
|
31
|
+
fi
|
|
32
|
+
|
|
33
|
+
echo "[$(date -Iseconds)] Server not responding (HTTP: ${HTTP_CODE}). Checking tmux..."
|
|
34
|
+
|
|
35
|
+
# Check if tmux session exists
|
|
36
|
+
if $TMUX_PATH has-session -t "=${SERVER_SESSION}" 2>/dev/null; then
|
|
37
|
+
echo "[$(date -Iseconds)] Session '${SERVER_SESSION}' exists but server not responding. Killing and restarting..."
|
|
38
|
+
$TMUX_PATH kill-session -t "=${SERVER_SESSION}" 2>/dev/null
|
|
39
|
+
sleep 2
|
|
40
|
+
fi
|
|
41
|
+
|
|
42
|
+
# Restart the server
|
|
43
|
+
CLI_PATH="${PROJECT_DIR}/node_modules/.bin/instar"
|
|
44
|
+
if [ ! -f "$CLI_PATH" ]; then
|
|
45
|
+
CLI_PATH=$(which instar 2>/dev/null)
|
|
46
|
+
fi
|
|
47
|
+
|
|
48
|
+
if [ -z "$CLI_PATH" ] || [ ! -f "$CLI_PATH" ]; then
|
|
49
|
+
echo "[$(date -Iseconds)] ERROR: instar CLI not found"
|
|
50
|
+
exit 1
|
|
51
|
+
fi
|
|
52
|
+
|
|
53
|
+
cd "$PROJECT_DIR" && $CLI_PATH server start
|
|
54
|
+
echo "[$(date -Iseconds)] Server restart initiated"
|
|
55
|
+
|
|
56
|
+
# Wait and verify
|
|
57
|
+
sleep 5
|
|
58
|
+
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" "http://localhost:${PORT}/health" 2>/dev/null)
|
|
59
|
+
if [ "$HTTP_CODE" = "200" ]; then
|
|
60
|
+
echo "[$(date -Iseconds)] Server recovered successfully"
|
|
61
|
+
else
|
|
62
|
+
echo "[$(date -Iseconds)] WARNING: Server still not responding after restart (HTTP: ${HTTP_CODE})"
|
|
63
|
+
fi
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# telegram-reply.sh — Send a message back to a Telegram topic via instar server.
|
|
3
|
+
#
|
|
4
|
+
# Usage:
|
|
5
|
+
# ./telegram-reply.sh TOPIC_ID "message text"
|
|
6
|
+
# echo "message text" | ./telegram-reply.sh TOPIC_ID
|
|
7
|
+
# cat <<'EOF' | ./telegram-reply.sh TOPIC_ID
|
|
8
|
+
# Multi-line message here
|
|
9
|
+
# EOF
|
|
10
|
+
#
|
|
11
|
+
# Reads INSTAR_PORT from environment (default: 4040).
|
|
12
|
+
|
|
13
|
+
TOPIC_ID="$1"
|
|
14
|
+
shift
|
|
15
|
+
|
|
16
|
+
if [ -z "$TOPIC_ID" ]; then
|
|
17
|
+
echo "Usage: telegram-reply.sh TOPIC_ID [message]" >&2
|
|
18
|
+
exit 1
|
|
19
|
+
fi
|
|
20
|
+
|
|
21
|
+
# Read message from args or stdin
|
|
22
|
+
if [ $# -gt 0 ]; then
|
|
23
|
+
MSG="$*"
|
|
24
|
+
else
|
|
25
|
+
MSG="$(cat)"
|
|
26
|
+
fi
|
|
27
|
+
|
|
28
|
+
if [ -z "$MSG" ]; then
|
|
29
|
+
echo "No message provided" >&2
|
|
30
|
+
exit 1
|
|
31
|
+
fi
|
|
32
|
+
|
|
33
|
+
PORT="${INSTAR_PORT:-4040}"
|
|
34
|
+
|
|
35
|
+
# Escape for JSON
|
|
36
|
+
JSON_MSG=$(printf '%s' "$MSG" | python3 -c 'import sys,json; print(json.dumps(sys.stdin.read()))' 2>/dev/null)
|
|
37
|
+
if [ -z "$JSON_MSG" ]; then
|
|
38
|
+
# Fallback if python3 not available: basic escape
|
|
39
|
+
JSON_MSG="\"$(printf '%s' "$MSG" | sed 's/\\/\\\\/g; s/"/\\"/g; s/\n/\\n/g')\""
|
|
40
|
+
fi
|
|
41
|
+
|
|
42
|
+
RESPONSE=$(curl -s -w "\n%{http_code}" -X POST "http://localhost:${PORT}/telegram/reply/${TOPIC_ID}" \
|
|
43
|
+
-H 'Content-Type: application/json' \
|
|
44
|
+
-d "{\"text\":${JSON_MSG}}")
|
|
45
|
+
|
|
46
|
+
HTTP_CODE=$(echo "$RESPONSE" | tail -1)
|
|
47
|
+
BODY=$(echo "$RESPONSE" | sed '$d')
|
|
48
|
+
|
|
49
|
+
if [ "$HTTP_CODE" = "200" ]; then
|
|
50
|
+
echo "Sent $(echo "$MSG" | wc -c | tr -d ' ') chars to topic $TOPIC_ID"
|
|
51
|
+
else
|
|
52
|
+
echo "Failed (HTTP $HTTP_CODE): $BODY" >&2
|
|
53
|
+
exit 1
|
|
54
|
+
fi
|