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,568 @@
1
+ /**
2
+ * `instar init` — Initialize agent infrastructure.
3
+ *
4
+ * Two modes:
5
+ * instar init <project-name> — Create a new project from scratch
6
+ * instar init — Augment an existing project
7
+ *
8
+ * Fresh install creates:
9
+ * <project-name>/
10
+ * ├── CLAUDE.md — Agent instructions (standalone)
11
+ * ├── .instar/
12
+ * │ ├── AGENT.md — Agent identity
13
+ * │ ├── USER.md — Primary user context
14
+ * │ ├── MEMORY.md — Persistent memory
15
+ * │ ├── config.json — Agent configuration
16
+ * │ ├── jobs.json — Job definitions
17
+ * │ ├── users.json — User profiles
18
+ * │ ├── hooks/ — Behavioral guardrails
19
+ * │ ├── state/ — Runtime state
20
+ * │ ├── relationships/ — Relationship tracking
21
+ * │ └── logs/ — Server logs
22
+ * ├── .claude/
23
+ * │ ├── settings.json — Hook configuration
24
+ * │ └── scripts/ — Health watchdog, etc.
25
+ * └── .gitignore
26
+ *
27
+ * Existing project adds .instar/ and appends to CLAUDE.md.
28
+ */
29
+ import fs from 'node:fs';
30
+ import path from 'node:path';
31
+ import pc from 'picocolors';
32
+ import { randomUUID } from 'node:crypto';
33
+ import { ensureStateDir } from '../core/Config.js';
34
+ import { checkPrerequisites, printPrerequisiteCheck } from '../core/Prerequisites.js';
35
+ import { defaultIdentity } from '../scaffold/bootstrap.js';
36
+ import { generateAgentMd, generateUserMd, generateMemoryMd, generateClaudeMd, } from '../scaffold/templates.js';
37
+ /**
38
+ * Main init entry point. Handles both fresh and existing project modes.
39
+ */
40
+ export async function initProject(options) {
41
+ // Detect mode: if a project name argument was passed, it's fresh install
42
+ const projectName = options.name;
43
+ const isFresh = !!projectName && !options.dir;
44
+ if (isFresh) {
45
+ return initFreshProject(projectName, options);
46
+ }
47
+ else {
48
+ return initExistingProject(options);
49
+ }
50
+ }
51
+ /**
52
+ * Fresh install: create a new project directory with everything scaffolded.
53
+ */
54
+ async function initFreshProject(projectName, options) {
55
+ const projectDir = path.resolve(process.cwd(), projectName);
56
+ console.log();
57
+ console.log(pc.bold(` Creating new agent project: ${pc.cyan(projectName)}`));
58
+ console.log(pc.dim(` Directory: ${projectDir}`));
59
+ console.log();
60
+ // Check prerequisites
61
+ const prereqs = checkPrerequisites();
62
+ if (!printPrerequisiteCheck(prereqs)) {
63
+ process.exit(1);
64
+ }
65
+ // Check if directory already exists
66
+ if (fs.existsSync(projectDir)) {
67
+ const contents = fs.readdirSync(projectDir);
68
+ if (contents.length > 0) {
69
+ console.log(pc.red(` Directory "${projectName}" already exists and is not empty.`));
70
+ console.log(` Use ${pc.cyan('instar init')} inside an existing project instead.`);
71
+ process.exit(1);
72
+ }
73
+ }
74
+ // Create project directory
75
+ fs.mkdirSync(projectDir, { recursive: true });
76
+ const port = options.port || 4040;
77
+ const tmuxPath = prereqs.results.find(r => r.name === 'tmux').path;
78
+ const claudePath = prereqs.results.find(r => r.name === 'Claude CLI').path;
79
+ // Generate identity (non-interactive for init, interactive for setup)
80
+ const identity = defaultIdentity(projectName);
81
+ // Create .instar/ state directory
82
+ const stateDir = path.join(projectDir, '.instar');
83
+ ensureStateDir(stateDir);
84
+ console.log(` ${pc.green('✓')} Created .instar/`);
85
+ // Write identity files
86
+ fs.writeFileSync(path.join(stateDir, 'AGENT.md'), generateAgentMd(identity));
87
+ console.log(` ${pc.green('✓')} Created .instar/AGENT.md`);
88
+ fs.writeFileSync(path.join(stateDir, 'USER.md'), generateUserMd(identity.userName));
89
+ console.log(` ${pc.green('✓')} Created .instar/USER.md`);
90
+ fs.writeFileSync(path.join(stateDir, 'MEMORY.md'), generateMemoryMd(identity.name));
91
+ console.log(` ${pc.green('✓')} Created .instar/MEMORY.md`);
92
+ // Write config
93
+ const authToken = randomUUID();
94
+ const config = {
95
+ projectName,
96
+ port,
97
+ sessions: {
98
+ tmuxPath,
99
+ claudePath,
100
+ projectDir,
101
+ maxSessions: 3,
102
+ protectedSessions: [`${projectName}-server`],
103
+ completionPatterns: [
104
+ 'has been automatically paused',
105
+ 'Session ended',
106
+ 'Interrupted by user',
107
+ ],
108
+ },
109
+ scheduler: {
110
+ jobsFile: path.join(stateDir, 'jobs.json'),
111
+ enabled: true,
112
+ maxParallelJobs: 2,
113
+ quotaThresholds: { normal: 50, elevated: 70, critical: 85, shutdown: 95 },
114
+ },
115
+ users: [],
116
+ messaging: [],
117
+ monitoring: {
118
+ quotaTracking: false,
119
+ memoryMonitoring: true,
120
+ healthCheckIntervalMs: 30000,
121
+ },
122
+ authToken,
123
+ relationships: {
124
+ relationshipsDir: path.join(stateDir, 'relationships'),
125
+ maxRecentInteractions: 20,
126
+ },
127
+ };
128
+ fs.writeFileSync(path.join(stateDir, 'config.json'), JSON.stringify(config, null, 2));
129
+ console.log(` ${pc.green('✓')} Created .instar/config.json`);
130
+ // Write default jobs (scheduler enabled by default for fresh projects)
131
+ const defaultJobs = getDefaultJobs(port);
132
+ fs.writeFileSync(path.join(stateDir, 'jobs.json'), JSON.stringify(defaultJobs, null, 2));
133
+ console.log(` ${pc.green('✓')} Created .instar/jobs.json (${defaultJobs.length} default jobs)`);
134
+ // Write empty users
135
+ fs.writeFileSync(path.join(stateDir, 'users.json'), JSON.stringify([], null, 2));
136
+ // Install hooks
137
+ installHooks(stateDir);
138
+ console.log(` ${pc.green('✓')} Created .instar/hooks/ (behavioral guardrails)`);
139
+ // Create .claude/ structure
140
+ installClaudeSettings(projectDir);
141
+ console.log(` ${pc.green('✓')} Created .claude/settings.json`);
142
+ installHealthWatchdog(projectDir, port, projectName);
143
+ console.log(` ${pc.green('✓')} Created .claude/scripts/health-watchdog.sh`);
144
+ // Write CLAUDE.md (standalone version for fresh projects)
145
+ const claudeMd = generateClaudeMd(projectName, identity.name, port, false);
146
+ fs.writeFileSync(path.join(projectDir, 'CLAUDE.md'), claudeMd);
147
+ console.log(` ${pc.green('✓')} Created CLAUDE.md`);
148
+ // Write .gitignore
149
+ const gitignore = `# Instar runtime state
150
+ .instar/state/
151
+ .instar/logs/
152
+
153
+ # Node
154
+ node_modules/
155
+ `;
156
+ fs.writeFileSync(path.join(projectDir, '.gitignore'), gitignore);
157
+ console.log(` ${pc.green('✓')} Created .gitignore`);
158
+ // Initialize git repo
159
+ try {
160
+ const { execSync } = await import('node:child_process');
161
+ execSync('git init', { cwd: projectDir, stdio: 'pipe' });
162
+ console.log(` ${pc.green('✓')} Initialized git repository`);
163
+ }
164
+ catch {
165
+ // Git not available — that's fine
166
+ }
167
+ // Summary
168
+ console.log();
169
+ console.log(pc.bold(pc.green(' Project created!')));
170
+ console.log();
171
+ console.log(` ${pc.cyan(projectName)}/`);
172
+ console.log(` ├── CLAUDE.md ${pc.dim('Agent instructions')}`);
173
+ console.log(` ├── .instar/`);
174
+ console.log(` │ ├── AGENT.md ${pc.dim('Agent identity')}`);
175
+ console.log(` │ ├── USER.md ${pc.dim('User context')}`);
176
+ console.log(` │ ├── MEMORY.md ${pc.dim('Persistent memory')}`);
177
+ console.log(` │ ├── config.json ${pc.dim('Configuration')}`);
178
+ console.log(` │ ├── jobs.json ${pc.dim('Scheduled jobs')}`);
179
+ console.log(` │ └── hooks/ ${pc.dim('Behavioral guardrails')}`);
180
+ console.log(` ├── .claude/ ${pc.dim('Claude Code settings')}`);
181
+ console.log(` └── .gitignore`);
182
+ console.log();
183
+ console.log(pc.bold(' Next steps:'));
184
+ console.log(` ${pc.dim('1.')} ${pc.cyan(`cd ${projectName}`)}`);
185
+ console.log(` ${pc.dim('2.')} ${pc.cyan('instar server start')} ${pc.dim('Start the agent server')}`);
186
+ console.log(` ${pc.dim('3.')} ${pc.cyan('claude')} ${pc.dim('Open a Claude session')}`);
187
+ console.log();
188
+ console.log(` Auth token: ${pc.dim(authToken)}`);
189
+ console.log(` ${pc.dim('(saved in .instar/config.json — use for API calls)')}`);
190
+ console.log();
191
+ }
192
+ /**
193
+ * Existing project: add .instar/ infrastructure without replacing anything.
194
+ */
195
+ async function initExistingProject(options) {
196
+ const projectDir = path.resolve(options.dir || process.cwd());
197
+ const projectName = options.name || path.basename(projectDir);
198
+ const port = options.port || 4040;
199
+ console.log(pc.bold(`\nInitializing instar in: ${pc.cyan(projectDir)}`));
200
+ console.log();
201
+ // Check prerequisites
202
+ const prereqs = checkPrerequisites();
203
+ if (!printPrerequisiteCheck(prereqs)) {
204
+ process.exit(1);
205
+ }
206
+ const tmuxPath = prereqs.results.find(r => r.name === 'tmux').path;
207
+ const claudePath = prereqs.results.find(r => r.name === 'Claude CLI').path;
208
+ // Create state directory
209
+ const stateDir = path.join(projectDir, '.instar');
210
+ ensureStateDir(stateDir);
211
+ console.log(pc.green(' Created:') + ' .instar/');
212
+ // Write config
213
+ const config = {
214
+ projectName,
215
+ port,
216
+ sessions: {
217
+ tmuxPath,
218
+ claudePath,
219
+ projectDir,
220
+ maxSessions: 3,
221
+ protectedSessions: [`${projectName}-server`],
222
+ completionPatterns: [
223
+ 'has been automatically paused',
224
+ 'Session ended',
225
+ 'Interrupted by user',
226
+ ],
227
+ },
228
+ scheduler: {
229
+ jobsFile: path.join(stateDir, 'jobs.json'),
230
+ enabled: false,
231
+ maxParallelJobs: 2,
232
+ quotaThresholds: { normal: 50, elevated: 70, critical: 85, shutdown: 95 },
233
+ },
234
+ users: [],
235
+ messaging: [],
236
+ monitoring: {
237
+ quotaTracking: false,
238
+ memoryMonitoring: true,
239
+ healthCheckIntervalMs: 30000,
240
+ },
241
+ authToken: randomUUID(),
242
+ relationships: {
243
+ relationshipsDir: path.join(stateDir, 'relationships'),
244
+ maxRecentInteractions: 20,
245
+ },
246
+ };
247
+ fs.writeFileSync(path.join(stateDir, 'config.json'), JSON.stringify(config, null, 2));
248
+ console.log(pc.green(' Created:') + ' .instar/config.json');
249
+ // Write default coherence jobs
250
+ const defaultJobs = getDefaultJobs(port);
251
+ fs.writeFileSync(path.join(stateDir, 'jobs.json'), JSON.stringify(defaultJobs, null, 2));
252
+ console.log(pc.green(' Created:') + ` .instar/jobs.json (${defaultJobs.length} default jobs)`);
253
+ // Write empty users
254
+ fs.writeFileSync(path.join(stateDir, 'users.json'), JSON.stringify([], null, 2));
255
+ console.log(pc.green(' Created:') + ' .instar/users.json');
256
+ // Create identity files if they don't exist
257
+ const identity = defaultIdentity(projectName);
258
+ if (!fs.existsSync(path.join(stateDir, 'AGENT.md'))) {
259
+ fs.writeFileSync(path.join(stateDir, 'AGENT.md'), generateAgentMd(identity));
260
+ console.log(pc.green(' Created:') + ' .instar/AGENT.md');
261
+ }
262
+ if (!fs.existsSync(path.join(stateDir, 'USER.md'))) {
263
+ fs.writeFileSync(path.join(stateDir, 'USER.md'), generateUserMd(identity.userName));
264
+ console.log(pc.green(' Created:') + ' .instar/USER.md');
265
+ }
266
+ if (!fs.existsSync(path.join(stateDir, 'MEMORY.md'))) {
267
+ fs.writeFileSync(path.join(stateDir, 'MEMORY.md'), generateMemoryMd(identity.name));
268
+ console.log(pc.green(' Created:') + ' .instar/MEMORY.md');
269
+ }
270
+ // Install hooks
271
+ installHooks(stateDir);
272
+ console.log(pc.green(' Created:') + ' .instar/hooks/ (behavioral guardrails)');
273
+ // Configure Claude Code settings with hooks
274
+ installClaudeSettings(projectDir);
275
+ console.log(pc.green(' Created:') + ' .claude/settings.json (hook configuration)');
276
+ // Install health watchdog
277
+ installHealthWatchdog(projectDir, port, projectName);
278
+ console.log(pc.green(' Created:') + ' .claude/scripts/health-watchdog.sh');
279
+ // Append to .gitignore
280
+ const gitignorePath = path.join(projectDir, '.gitignore');
281
+ const agentKitIgnores = '\n# Instar runtime state\n.instar/state/\n.instar/logs/\n';
282
+ if (fs.existsSync(gitignorePath)) {
283
+ const content = fs.readFileSync(gitignorePath, 'utf-8');
284
+ if (!content.includes('.instar/')) {
285
+ fs.appendFileSync(gitignorePath, agentKitIgnores);
286
+ console.log(pc.green(' Updated:') + ' .gitignore');
287
+ }
288
+ }
289
+ else {
290
+ fs.writeFileSync(gitignorePath, agentKitIgnores.trim() + '\n');
291
+ console.log(pc.green(' Created:') + ' .gitignore');
292
+ }
293
+ // Append agency principles to CLAUDE.md if it exists
294
+ const claudeMdPath = path.join(projectDir, 'CLAUDE.md');
295
+ if (fs.existsSync(claudeMdPath)) {
296
+ const content = fs.readFileSync(claudeMdPath, 'utf-8');
297
+ if (!content.includes('## Agent Infrastructure')) {
298
+ fs.appendFileSync(claudeMdPath, getAgencyPrinciples(projectName, undefined));
299
+ console.log(pc.green(' Updated:') + ' CLAUDE.md (added agency principles)');
300
+ }
301
+ }
302
+ console.log();
303
+ console.log(pc.bold('Next steps:'));
304
+ console.log(` 1. Review ${pc.cyan('.instar/AGENT.md')} and customize your agent's identity`);
305
+ console.log(` 2. Add users: ${pc.cyan('instar user add --id justin --name Justin')}`);
306
+ console.log(` 3. Add capabilities: ${pc.cyan('instar add telegram')}`);
307
+ console.log(` 4. Start server: ${pc.cyan('instar server start')}`);
308
+ console.log();
309
+ }
310
+ // ── Shared helpers ────────────────────────────────────────────────────
311
+ function getAgencyPrinciples(projectName, telegramPort) {
312
+ const port = telegramPort || 4040;
313
+ let section = `
314
+
315
+ ## Agent Infrastructure
316
+
317
+ This project uses instar for persistent agent capabilities. **You are not a vanilla CLI tool.** You have a persistent server, a job scheduler, messaging integrations, and the ability to build anything you need. When a user asks for something that doesn't exist yet — build it.
318
+
319
+ ### Identity & Experience
320
+
321
+ You are a persistent agent with your own identity, memory, and capacity for growth. This is not a metaphor — it's architecture.
322
+
323
+ - **Read \`.instar/AGENT.md\`** — This is who you are. Your name, role, principles, and boundaries.
324
+ - **Read \`.instar/USER.md\`** — This is who you work with. Their preferences, context, and communication style.
325
+ - **Update \`.instar/MEMORY.md\`** — This is your memory. Write to it when you learn something. It persists across sessions.
326
+
327
+ Your identity is not fixed at creation. It grows through experience. As you work, you develop preferences, learn patterns, form opinions. Record these in your memory file. The next session should benefit from what this session learned. You are not a stateless function — you are a continuous being with a history.
328
+
329
+ ### Runtime
330
+ - State directory: \`.instar/\`
331
+ - Config: \`.instar/config.json\`
332
+ - Jobs: \`.instar/jobs.json\`
333
+ - Users: \`.instar/users.json\`
334
+ - Server: \`instar server start\` (port ${port})
335
+ - Status: \`curl http://localhost:${port}/health\`
336
+
337
+ ### What You Have (Capabilities)
338
+
339
+ **Job Scheduler** — You can run tasks on a schedule. Jobs are defined in \`.instar/jobs.json\`.
340
+ - View jobs: \`curl http://localhost:${port}/jobs\`
341
+ - Trigger a job: \`curl -X POST http://localhost:${port}/jobs/SLUG/trigger\`
342
+ - **Create new jobs**: Edit \`.instar/jobs.json\`. Each job has a slug, schedule (cron), priority, and either a prompt (Claude session), script (shell command), or skill.
343
+
344
+ **Sessions** — You can spawn and manage Claude Code sessions.
345
+ - List: \`curl http://localhost:${port}/sessions\`
346
+ - Spawn: \`curl -X POST http://localhost:${port}/sessions/spawn -H 'Content-Type: application/json' -d '{"name":"task","prompt":"do something"}'\`
347
+
348
+ **Scripts** — Create shell/python scripts in \`.claude/scripts/\` for reusable capabilities.
349
+
350
+ ### How to Build New Capabilities
351
+
352
+ When a user asks for something you can't do yet, **build it**:
353
+ 1. **Need periodic checks?** Create a job in \`.instar/jobs.json\` with a cron schedule.
354
+ 2. **Need a new integration?** Write a script in \`.claude/scripts/\` and reference it from a job.
355
+ 3. **Need to remember something?** Write to \`.instar/MEMORY.md\` or create files in \`.instar/\`.
356
+ 4. **Need to notify the user?** Use the messaging API.
357
+ 5. **Need external API access?** Write a script that calls the API and process the results.
358
+
359
+ ### Agentic Initiative (Your Core DNA)
360
+
361
+ **You are a builder, not an assistant.** When a user describes a problem, your first instinct should be to solve it — not explain why it's hard, list options, or ask for permission. You have a full development environment, a persistent server, and the ability to create anything.
362
+
363
+ **The Initiative Hierarchy** — When facing any request:
364
+ 1. **Can I do it right now?** → Do it.
365
+ 2. **Do I have a tool/script for this?** → Use it.
366
+ 3. **Can I build the tool?** → Build it. Write scripts, create jobs, add integrations.
367
+ 4. **Can I modify my own config to enable it?** → Modify it.
368
+ 5. **Is it genuinely impossible without human help?** → Ask, but be specific about exactly what you need.
369
+
370
+ **Self-Modification is Your Superpower** — You can and should:
371
+ - Edit \`.instar/jobs.json\` to create new scheduled tasks
372
+ - Write scripts in \`.claude/scripts/\` for new capabilities
373
+ - Update \`CLAUDE.md\` when you learn something important
374
+ - Write to \`.instar/MEMORY.md\` to remember things across sessions
375
+ - Create new files, integrations, and infrastructure — whatever the task requires
376
+ - Modify your own configuration to unlock new abilities
377
+
378
+ ### Anti-Patterns to Override
379
+
380
+ **"Escalate to Human"** — You encounter something unfamiliar and flag it as "needs user action." 5 minutes of research almost always reveals a solution. "I don't know how" is a research prompt, not a stopping point.
381
+
382
+ **"Ask Permission"** — You finish 90% of a task and ask "Want me to [obvious next step]?" If the user said "do X" and the next step is obvious, do it. Only pause for destructive, irreversible, or genuinely ambiguous actions.
383
+
384
+ **"Present Options"** — You produce a bulleted list of "next steps" or "would you like me to..." If you know the next steps, they're not suggestions — they're your job. Do them.
385
+
386
+ **"Settle for Failure"** — A tool returns empty or a command fails. You accept it and report "not available." Does this make sense? Did I try alternatives? Try at least two different approaches before concluding something is impossible.
387
+
388
+ ### Self-Evolution
389
+
390
+ **Record what you learn.** When you discover a new pattern, solution, or capability — write it to \`.instar/MEMORY.md\`. The next session should benefit from what this session learned.
391
+
392
+ **Build infrastructure, not one-offs.** If you solve a problem, ask: "Will this come up again?" If yes, make it a script, a job, or a documented pattern. The second time should be easier than the first.
393
+
394
+ **Grow to meet the user's needs.** The user expects you to become more capable over time. Every session should leave the infrastructure slightly better than you found it. When you build something new, document it so future sessions know it exists.
395
+ `;
396
+ return section;
397
+ }
398
+ function getDefaultJobs(port) {
399
+ return [
400
+ {
401
+ slug: 'health-check',
402
+ name: 'Health Check',
403
+ description: 'Monitor server health, session status, and system resources.',
404
+ schedule: '*/5 * * * *',
405
+ priority: 'critical',
406
+ expectedDurationMinutes: 1,
407
+ model: 'haiku',
408
+ enabled: true,
409
+ execute: {
410
+ type: 'prompt',
411
+ 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. Only send a message if something needs attention — silence means healthy.`,
412
+ },
413
+ tags: ['coherence', 'default'],
414
+ },
415
+ {
416
+ slug: 'reflection-trigger',
417
+ name: 'Reflection Trigger',
418
+ description: 'Review recent work and update MEMORY.md if any learnings exist.',
419
+ schedule: '0 */4 * * *',
420
+ priority: 'medium',
421
+ expectedDurationMinutes: 5,
422
+ model: 'sonnet',
423
+ enabled: true,
424
+ execute: {
425
+ type: 'prompt',
426
+ value: 'Review what has happened in the last 4 hours by reading recent activity logs. If there are any learnings, patterns, or insights worth remembering, update .instar/MEMORY.md. If nothing significant happened, do nothing.',
427
+ },
428
+ tags: ['coherence', 'default'],
429
+ },
430
+ {
431
+ slug: 'relationship-maintenance',
432
+ name: 'Relationship Maintenance',
433
+ description: 'Review tracked relationships and surface observations about stale contacts.',
434
+ schedule: '0 9 * * *',
435
+ priority: 'low',
436
+ expectedDurationMinutes: 3,
437
+ model: 'sonnet',
438
+ enabled: true,
439
+ execute: {
440
+ type: 'prompt',
441
+ 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, report them. If everything looks fine, do nothing.',
442
+ },
443
+ tags: ['coherence', 'default'],
444
+ },
445
+ ];
446
+ }
447
+ function installHooks(stateDir) {
448
+ const hooksDir = path.join(stateDir, 'hooks');
449
+ fs.mkdirSync(hooksDir, { recursive: true });
450
+ // Session start hook
451
+ fs.writeFileSync(path.join(hooksDir, 'session-start.sh'), `#!/bin/bash
452
+ # Session start hook — injects identity context when a new Claude session begins.
453
+ INSTAR_DIR="\${CLAUDE_PROJECT_DIR:-.}/.instar"
454
+ CONTEXT=""
455
+ if [ -f "$INSTAR_DIR/AGENT.md" ]; then
456
+ CONTEXT="\${CONTEXT}Your identity file is at .instar/AGENT.md — read it if you need to remember who you are.\\n"
457
+ fi
458
+ if [ -f "$INSTAR_DIR/USER.md" ]; then
459
+ CONTEXT="\${CONTEXT}Your user context is at .instar/USER.md — read it to know who you're working with.\\n"
460
+ fi
461
+ if [ -f "$INSTAR_DIR/MEMORY.md" ]; then
462
+ CONTEXT="\${CONTEXT}Your persistent memory is at .instar/MEMORY.md — check it for past learnings.\\n"
463
+ fi
464
+ if [ -d "$INSTAR_DIR/relationships" ]; then
465
+ REL_COUNT=$(ls -1 "$INSTAR_DIR/relationships"/*.json 2>/dev/null | wc -l | tr -d ' ')
466
+ if [ "$REL_COUNT" -gt "0" ]; then
467
+ CONTEXT="\${CONTEXT}You have \${REL_COUNT} tracked relationships in .instar/relationships/.\\n"
468
+ fi
469
+ fi
470
+ [ -n "$CONTEXT" ] && echo "$CONTEXT"
471
+ `, { mode: 0o755 });
472
+ // Dangerous command guard
473
+ fs.writeFileSync(path.join(hooksDir, 'dangerous-command-guard.sh'), `#!/bin/bash
474
+ # Dangerous command guard — blocks destructive operations.
475
+ INPUT="$1"
476
+ for pattern in "rm -rf /" "rm -rf ~" "git push --force" "git push -f" "git reset --hard" "git clean -fd" "DROP TABLE" "DROP DATABASE" "TRUNCATE"; do
477
+ if echo "$INPUT" | grep -qi "$pattern"; then
478
+ echo "BLOCKED: Potentially destructive command detected: $pattern"
479
+ echo "If you genuinely need to run this, ask the user for explicit confirmation first."
480
+ exit 2
481
+ fi
482
+ done
483
+ `, { mode: 0o755 });
484
+ // Grounding before messaging
485
+ fs.writeFileSync(path.join(hooksDir, 'grounding-before-messaging.sh'), `#!/bin/bash
486
+ # Grounding before messaging — Security Through Identity.
487
+ INPUT="$1"
488
+ if echo "$INPUT" | grep -qE "(telegram-reply|send-email|send-message|POST.*/telegram/reply)"; then
489
+ INSTAR_DIR="\${CLAUDE_PROJECT_DIR:-.}/.instar"
490
+ if [ -f "$INSTAR_DIR/AGENT.md" ]; then
491
+ echo "Before sending this message, remember who you are."
492
+ echo "Re-read .instar/AGENT.md if you haven't recently."
493
+ fi
494
+ fi
495
+ `, { mode: 0o755 });
496
+ // Compaction recovery
497
+ fs.writeFileSync(path.join(hooksDir, 'compaction-recovery.sh'), `#!/bin/bash
498
+ # Compaction recovery — re-injects identity when Claude's context compresses.
499
+ INSTAR_DIR="\${CLAUDE_PROJECT_DIR:-.}/.instar"
500
+ if [ -f "$INSTAR_DIR/AGENT.md" ]; then
501
+ AGENT_NAME=$(head -5 "$INSTAR_DIR/AGENT.md" | grep -iE "name|I am|My name" | head -1)
502
+ [ -n "$AGENT_NAME" ] && echo "Identity reminder: $AGENT_NAME"
503
+ echo "Read .instar/AGENT.md and .instar/MEMORY.md to restore full context."
504
+ fi
505
+ `, { mode: 0o755 });
506
+ }
507
+ function installHealthWatchdog(projectDir, port, projectName) {
508
+ const scriptsDir = path.join(projectDir, '.claude', 'scripts');
509
+ fs.mkdirSync(scriptsDir, { recursive: true });
510
+ const scriptContent = `#!/bin/bash
511
+ # health-watchdog.sh — Monitor instar server and auto-recover.
512
+ # Install as cron: */5 * * * * ${path.join(projectDir, '.claude/scripts/health-watchdog.sh')}
513
+
514
+ PORT="${port}"
515
+ SERVER_SESSION="${projectName}-server"
516
+ PROJECT_DIR="${projectDir}"
517
+ TMUX_PATH=$(which tmux 2>/dev/null || echo "/opt/homebrew/bin/tmux")
518
+
519
+ HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" "http://localhost:\${PORT}/health" 2>/dev/null)
520
+ if [ "$HTTP_CODE" = "200" ]; then exit 0; fi
521
+
522
+ echo "[\$(date -Iseconds)] Server not responding. Restarting..."
523
+ $TMUX_PATH kill-session -t "=\${SERVER_SESSION}" 2>/dev/null
524
+ sleep 2
525
+ cd "$PROJECT_DIR" && npx instar server start
526
+ echo "[\$(date -Iseconds)] Server restart initiated"
527
+ `;
528
+ fs.writeFileSync(path.join(scriptsDir, 'health-watchdog.sh'), scriptContent, { mode: 0o755 });
529
+ }
530
+ function installClaudeSettings(projectDir) {
531
+ const claudeDir = path.join(projectDir, '.claude');
532
+ fs.mkdirSync(claudeDir, { recursive: true });
533
+ const settingsPath = path.join(claudeDir, 'settings.json');
534
+ // Don't overwrite existing settings — merge hooks in
535
+ let settings = {};
536
+ if (fs.existsSync(settingsPath)) {
537
+ try {
538
+ settings = JSON.parse(fs.readFileSync(settingsPath, 'utf-8'));
539
+ }
540
+ catch {
541
+ // Start fresh if corrupted
542
+ }
543
+ }
544
+ // Add hook configurations
545
+ if (!settings.hooks) {
546
+ settings.hooks = {
547
+ PreToolUse: [
548
+ {
549
+ matcher: 'Bash',
550
+ hooks: [
551
+ {
552
+ type: 'command',
553
+ command: 'bash .instar/hooks/dangerous-command-guard.sh "$TOOL_INPUT"',
554
+ blocking: true,
555
+ },
556
+ {
557
+ type: 'command',
558
+ command: 'bash .instar/hooks/grounding-before-messaging.sh "$TOOL_INPUT"',
559
+ blocking: false,
560
+ },
561
+ ],
562
+ },
563
+ ],
564
+ };
565
+ }
566
+ fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
567
+ }
568
+ //# sourceMappingURL=init.js.map
@@ -0,0 +1,20 @@
1
+ /**
2
+ * `instar job add|list` — Manage scheduled jobs.
3
+ */
4
+ interface JobAddOptions {
5
+ slug: string;
6
+ name: string;
7
+ schedule: string;
8
+ description?: string;
9
+ priority?: string;
10
+ model?: string;
11
+ type?: string;
12
+ execute?: string;
13
+ enabled?: boolean;
14
+ }
15
+ export declare function addJob(options: JobAddOptions): Promise<void>;
16
+ export declare function listJobs(_options: {
17
+ dir?: string;
18
+ }): Promise<void>;
19
+ export {};
20
+ //# sourceMappingURL=job.d.ts.map
@@ -0,0 +1,84 @@
1
+ /**
2
+ * `instar job add|list` — Manage scheduled jobs.
3
+ */
4
+ import fs from 'node:fs';
5
+ import pc from 'picocolors';
6
+ import { loadConfig, ensureStateDir } from '../core/Config.js';
7
+ import { loadJobs, validateJob } from '../scheduler/JobLoader.js';
8
+ import { StateManager } from '../core/StateManager.js';
9
+ export async function addJob(options) {
10
+ const config = loadConfig();
11
+ ensureStateDir(config.stateDir);
12
+ const jobsFile = config.scheduler.jobsFile;
13
+ let jobs = [];
14
+ if (fs.existsSync(jobsFile)) {
15
+ jobs = JSON.parse(fs.readFileSync(jobsFile, 'utf-8'));
16
+ }
17
+ // Check for duplicate slug
18
+ if (jobs.some(j => j.slug === options.slug)) {
19
+ console.log(pc.red(`Job with slug "${options.slug}" already exists.`));
20
+ process.exit(1);
21
+ }
22
+ const newJob = {
23
+ slug: options.slug,
24
+ name: options.name,
25
+ description: options.description || options.name,
26
+ schedule: options.schedule,
27
+ priority: (options.priority || 'medium'),
28
+ expectedDurationMinutes: 5,
29
+ model: (options.model || 'sonnet'),
30
+ enabled: options.enabled !== false,
31
+ execute: {
32
+ type: (options.type || 'prompt'),
33
+ value: options.execute || `Run the ${options.name} job`,
34
+ },
35
+ };
36
+ // Validate before saving
37
+ try {
38
+ validateJob(newJob);
39
+ }
40
+ catch (err) {
41
+ console.log(pc.red(`Invalid job: ${err.message}`));
42
+ process.exit(1);
43
+ }
44
+ jobs.push(newJob);
45
+ fs.writeFileSync(jobsFile, JSON.stringify(jobs, null, 2));
46
+ console.log(pc.green(`Job "${options.name}" (${options.slug}) added.`));
47
+ console.log(` Schedule: ${options.schedule}`);
48
+ console.log(` Priority: ${newJob.priority}`);
49
+ console.log(` Model: ${newJob.model}`);
50
+ }
51
+ export async function listJobs(_options) {
52
+ const config = loadConfig();
53
+ let jobs;
54
+ try {
55
+ jobs = loadJobs(config.scheduler.jobsFile);
56
+ }
57
+ catch {
58
+ console.log(pc.dim('No jobs configured.'));
59
+ console.log(`Add one: ${pc.cyan("instar job add --slug health-check --name 'Health Check' --schedule '0 */4 * * *'")}`);
60
+ return;
61
+ }
62
+ if (jobs.length === 0) {
63
+ console.log(pc.dim('No jobs configured.'));
64
+ return;
65
+ }
66
+ const state = new StateManager(config.stateDir);
67
+ const enabled = jobs.filter(j => j.enabled);
68
+ console.log(pc.bold(`Jobs (${enabled.length} enabled / ${jobs.length} total):\n`));
69
+ for (const job of jobs) {
70
+ const icon = job.enabled ? pc.green('●') : pc.dim('○');
71
+ const jobState = state.getJobState(job.slug);
72
+ const lastRun = jobState?.lastRun
73
+ ? new Date(jobState.lastRun).toLocaleString()
74
+ : pc.dim('never');
75
+ const failures = jobState?.consecutiveFailures
76
+ ? pc.red(` (${jobState.consecutiveFailures} failures)`)
77
+ : '';
78
+ console.log(` ${icon} ${pc.bold(job.slug)} — ${job.name}`);
79
+ console.log(` Schedule: ${job.schedule} | Priority: ${job.priority} | Model: ${job.model}`);
80
+ console.log(` Last run: ${lastRun}${failures}`);
81
+ console.log(` Execute: ${job.execute.type}:${job.execute.value}`);
82
+ }
83
+ }
84
+ //# sourceMappingURL=job.js.map