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,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
|