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.
Files changed (115) hide show
  1. package/.claude/settings.local.json +7 -0
  2. package/.claude/skills/setup-wizard/skill.md +343 -0
  3. package/.github/workflows/ci.yml +78 -0
  4. package/CLAUDE.md +82 -0
  5. package/README.md +194 -0
  6. package/dist/cli.d.ts +18 -0
  7. package/dist/cli.js +141 -0
  8. package/dist/commands/init.d.ts +40 -0
  9. package/dist/commands/init.js +568 -0
  10. package/dist/commands/job.d.ts +20 -0
  11. package/dist/commands/job.js +84 -0
  12. package/dist/commands/server.d.ts +19 -0
  13. package/dist/commands/server.js +273 -0
  14. package/dist/commands/setup.d.ts +24 -0
  15. package/dist/commands/setup.js +865 -0
  16. package/dist/commands/status.d.ts +11 -0
  17. package/dist/commands/status.js +114 -0
  18. package/dist/commands/user.d.ts +17 -0
  19. package/dist/commands/user.js +53 -0
  20. package/dist/core/Config.d.ts +16 -0
  21. package/dist/core/Config.js +144 -0
  22. package/dist/core/Prerequisites.d.ts +28 -0
  23. package/dist/core/Prerequisites.js +159 -0
  24. package/dist/core/RelationshipManager.d.ts +73 -0
  25. package/dist/core/RelationshipManager.js +318 -0
  26. package/dist/core/SessionManager.d.ts +89 -0
  27. package/dist/core/SessionManager.js +326 -0
  28. package/dist/core/StateManager.d.ts +28 -0
  29. package/dist/core/StateManager.js +96 -0
  30. package/dist/core/types.d.ts +279 -0
  31. package/dist/core/types.js +8 -0
  32. package/dist/index.d.ts +18 -0
  33. package/dist/index.js +23 -0
  34. package/dist/messaging/TelegramAdapter.d.ts +73 -0
  35. package/dist/messaging/TelegramAdapter.js +288 -0
  36. package/dist/monitoring/HealthChecker.d.ts +38 -0
  37. package/dist/monitoring/HealthChecker.js +148 -0
  38. package/dist/scaffold/bootstrap.d.ts +21 -0
  39. package/dist/scaffold/bootstrap.js +110 -0
  40. package/dist/scaffold/templates.d.ts +34 -0
  41. package/dist/scaffold/templates.js +187 -0
  42. package/dist/scheduler/JobLoader.d.ts +18 -0
  43. package/dist/scheduler/JobLoader.js +70 -0
  44. package/dist/scheduler/JobScheduler.d.ts +111 -0
  45. package/dist/scheduler/JobScheduler.js +402 -0
  46. package/dist/server/AgentServer.d.ts +40 -0
  47. package/dist/server/AgentServer.js +73 -0
  48. package/dist/server/middleware.d.ts +12 -0
  49. package/dist/server/middleware.js +50 -0
  50. package/dist/server/routes.d.ts +25 -0
  51. package/dist/server/routes.js +224 -0
  52. package/dist/users/UserManager.d.ts +45 -0
  53. package/dist/users/UserManager.js +113 -0
  54. package/docs/dawn-audit-report.md +412 -0
  55. package/docs/positioning-vs-openclaw.md +246 -0
  56. package/package.json +52 -0
  57. package/src/cli.ts +169 -0
  58. package/src/commands/init.ts +654 -0
  59. package/src/commands/job.ts +110 -0
  60. package/src/commands/server.ts +325 -0
  61. package/src/commands/setup.ts +958 -0
  62. package/src/commands/status.ts +125 -0
  63. package/src/commands/user.ts +71 -0
  64. package/src/core/Config.ts +161 -0
  65. package/src/core/Prerequisites.ts +187 -0
  66. package/src/core/RelationshipManager.ts +366 -0
  67. package/src/core/SessionManager.ts +385 -0
  68. package/src/core/StateManager.ts +121 -0
  69. package/src/core/types.ts +320 -0
  70. package/src/index.ts +58 -0
  71. package/src/messaging/TelegramAdapter.ts +365 -0
  72. package/src/monitoring/HealthChecker.ts +172 -0
  73. package/src/scaffold/bootstrap.ts +122 -0
  74. package/src/scaffold/templates.ts +204 -0
  75. package/src/scheduler/JobLoader.ts +85 -0
  76. package/src/scheduler/JobScheduler.ts +476 -0
  77. package/src/server/AgentServer.ts +93 -0
  78. package/src/server/middleware.ts +58 -0
  79. package/src/server/routes.ts +278 -0
  80. package/src/templates/default-jobs.json +47 -0
  81. package/src/templates/hooks/compaction-recovery.sh +23 -0
  82. package/src/templates/hooks/dangerous-command-guard.sh +35 -0
  83. package/src/templates/hooks/grounding-before-messaging.sh +22 -0
  84. package/src/templates/hooks/session-start.sh +37 -0
  85. package/src/templates/hooks/settings-template.json +45 -0
  86. package/src/templates/scripts/health-watchdog.sh +63 -0
  87. package/src/templates/scripts/telegram-reply.sh +54 -0
  88. package/src/users/UserManager.ts +129 -0
  89. package/tests/e2e/lifecycle.test.ts +376 -0
  90. package/tests/fixtures/test-repo/CLAUDE.md +3 -0
  91. package/tests/fixtures/test-repo/README.md +1 -0
  92. package/tests/helpers/setup.ts +209 -0
  93. package/tests/integration/fresh-install.test.ts +218 -0
  94. package/tests/integration/scheduler-basic.test.ts +109 -0
  95. package/tests/integration/server-full.test.ts +284 -0
  96. package/tests/integration/session-lifecycle.test.ts +181 -0
  97. package/tests/unit/Config.test.ts +22 -0
  98. package/tests/unit/HealthChecker.test.ts +168 -0
  99. package/tests/unit/JobLoader.test.ts +151 -0
  100. package/tests/unit/JobScheduler.test.ts +267 -0
  101. package/tests/unit/Prerequisites.test.ts +59 -0
  102. package/tests/unit/RelationshipManager.test.ts +345 -0
  103. package/tests/unit/StateManager.test.ts +143 -0
  104. package/tests/unit/TelegramAdapter.test.ts +165 -0
  105. package/tests/unit/UserManager.test.ts +131 -0
  106. package/tests/unit/bootstrap.test.ts +28 -0
  107. package/tests/unit/commands.test.ts +138 -0
  108. package/tests/unit/middleware.test.ts +92 -0
  109. package/tests/unit/relationship-routes.test.ts +131 -0
  110. package/tests/unit/scaffold-templates.test.ts +132 -0
  111. package/tests/unit/server.test.ts +163 -0
  112. package/tsconfig.json +20 -0
  113. package/vitest.config.ts +9 -0
  114. package/vitest.e2e.config.ts +9 -0
  115. 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