@josharsh/demon-cli 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/LICENSE +21 -0
- package/README.md +130 -0
- package/dist/actions/base.d.ts +4 -0
- package/dist/actions/base.d.ts.map +1 -0
- package/dist/actions/base.js +32 -0
- package/dist/actions/base.js.map +1 -0
- package/dist/actions/file.d.ts +7 -0
- package/dist/actions/file.d.ts.map +1 -0
- package/dist/actions/file.js +35 -0
- package/dist/actions/file.js.map +1 -0
- package/dist/actions/github.d.ts +7 -0
- package/dist/actions/github.d.ts.map +1 -0
- package/dist/actions/github.js +125 -0
- package/dist/actions/github.js.map +1 -0
- package/dist/actions/index.d.ts +5 -0
- package/dist/actions/index.d.ts.map +1 -0
- package/dist/actions/index.js +22 -0
- package/dist/actions/index.js.map +1 -0
- package/dist/actions/log.d.ts +7 -0
- package/dist/actions/log.d.ts.map +1 -0
- package/dist/actions/log.js +30 -0
- package/dist/actions/log.js.map +1 -0
- package/dist/actions/notify.d.ts +11 -0
- package/dist/actions/notify.d.ts.map +1 -0
- package/dist/actions/notify.js +34 -0
- package/dist/actions/notify.js.map +1 -0
- package/dist/actions/script.d.ts +7 -0
- package/dist/actions/script.d.ts.map +1 -0
- package/dist/actions/script.js +46 -0
- package/dist/actions/script.js.map +1 -0
- package/dist/actions/slack.d.ts +7 -0
- package/dist/actions/slack.d.ts.map +1 -0
- package/dist/actions/slack.js +47 -0
- package/dist/actions/slack.js.map +1 -0
- package/dist/actions/webhook.d.ts +7 -0
- package/dist/actions/webhook.d.ts.map +1 -0
- package/dist/actions/webhook.js +41 -0
- package/dist/actions/webhook.js.map +1 -0
- package/dist/adapters/clock.d.ts +3 -0
- package/dist/adapters/clock.d.ts.map +1 -0
- package/dist/adapters/clock.js +8 -0
- package/dist/adapters/clock.js.map +1 -0
- package/dist/adapters/fs-kv.d.ts +3 -0
- package/dist/adapters/fs-kv.d.ts.map +1 -0
- package/dist/adapters/fs-kv.js +34 -0
- package/dist/adapters/fs-kv.js.map +1 -0
- package/dist/adapters/keys.d.ts +3 -0
- package/dist/adapters/keys.d.ts.map +1 -0
- package/dist/adapters/keys.js +17 -0
- package/dist/adapters/keys.js.map +1 -0
- package/dist/commands/ensure-provider.d.ts +7 -0
- package/dist/commands/ensure-provider.d.ts.map +1 -0
- package/dist/commands/ensure-provider.js +121 -0
- package/dist/commands/ensure-provider.js.map +1 -0
- package/dist/commands/init.d.ts +2 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +196 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/inspect.d.ts +7 -0
- package/dist/commands/inspect.d.ts.map +1 -0
- package/dist/commands/inspect.js +97 -0
- package/dist/commands/inspect.js.map +1 -0
- package/dist/commands/install.d.ts +3 -0
- package/dist/commands/install.d.ts.map +1 -0
- package/dist/commands/install.js +192 -0
- package/dist/commands/install.js.map +1 -0
- package/dist/commands/logs.d.ts +8 -0
- package/dist/commands/logs.d.ts.map +1 -0
- package/dist/commands/logs.js +103 -0
- package/dist/commands/logs.js.map +1 -0
- package/dist/commands/providers.d.ts +5 -0
- package/dist/commands/providers.d.ts.map +1 -0
- package/dist/commands/providers.js +251 -0
- package/dist/commands/providers.js.map +1 -0
- package/dist/commands/ps.d.ts +2 -0
- package/dist/commands/ps.d.ts.map +1 -0
- package/dist/commands/ps.js +87 -0
- package/dist/commands/ps.js.map +1 -0
- package/dist/commands/secrets.d.ts +9 -0
- package/dist/commands/secrets.d.ts.map +1 -0
- package/dist/commands/secrets.js +97 -0
- package/dist/commands/secrets.js.map +1 -0
- package/dist/commands/start.d.ts +10 -0
- package/dist/commands/start.d.ts.map +1 -0
- package/dist/commands/start.js +238 -0
- package/dist/commands/start.js.map +1 -0
- package/dist/commands/status.d.ts +2 -0
- package/dist/commands/status.d.ts.map +1 -0
- package/dist/commands/status.js +69 -0
- package/dist/commands/status.js.map +1 -0
- package/dist/commands/stop.d.ts +6 -0
- package/dist/commands/stop.d.ts.map +1 -0
- package/dist/commands/stop.js +43 -0
- package/dist/commands/stop.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +253 -0
- package/dist/index.js.map +1 -0
- package/dist/providers/config.d.ts +8 -0
- package/dist/providers/config.d.ts.map +1 -0
- package/dist/providers/config.js +31 -0
- package/dist/providers/config.js.map +1 -0
- package/dist/providers/credentials.d.ts +5 -0
- package/dist/providers/credentials.d.ts.map +1 -0
- package/dist/providers/credentials.js +69 -0
- package/dist/providers/credentials.js.map +1 -0
- package/dist/providers/registry.d.ts +12 -0
- package/dist/providers/registry.d.ts.map +1 -0
- package/dist/providers/registry.js +58 -0
- package/dist/providers/registry.js.map +1 -0
- package/dist/providers/secrets.d.ts +9 -0
- package/dist/providers/secrets.d.ts.map +1 -0
- package/dist/providers/secrets.js +44 -0
- package/dist/providers/secrets.js.map +1 -0
- package/dist/providers/system-auth.d.ts +8 -0
- package/dist/providers/system-auth.d.ts.map +1 -0
- package/dist/providers/system-auth.js +168 -0
- package/dist/providers/system-auth.js.map +1 -0
- package/dist/runtime/daemon.d.ts +10 -0
- package/dist/runtime/daemon.d.ts.map +1 -0
- package/dist/runtime/daemon.js +264 -0
- package/dist/runtime/daemon.js.map +1 -0
- package/dist/runtime/supervisor.d.ts +6 -0
- package/dist/runtime/supervisor.d.ts.map +1 -0
- package/dist/runtime/supervisor.js +45 -0
- package/dist/runtime/supervisor.js.map +1 -0
- package/dist/types.d.ts +27 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +7 -0
- package/dist/types.js.map +1 -0
- package/dist/utils/logger.d.ts +4 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +44 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/manifest.d.ts +5 -0
- package/dist/utils/manifest.d.ts.map +1 -0
- package/dist/utils/manifest.js +276 -0
- package/dist/utils/manifest.js.map +1 -0
- package/dist/utils/paths.d.ts +13 -0
- package/dist/utils/paths.d.ts.map +1 -0
- package/dist/utils/paths.js +52 -0
- package/dist/utils/paths.js.map +1 -0
- package/dist/utils/time.d.ts +11 -0
- package/dist/utils/time.d.ts.map +1 -0
- package/dist/utils/time.js +40 -0
- package/dist/utils/time.js.map +1 -0
- package/dist/watchers/base.d.ts +3 -0
- package/dist/watchers/base.d.ts.map +1 -0
- package/dist/watchers/base.js +20 -0
- package/dist/watchers/base.js.map +1 -0
- package/dist/watchers/filesystem.d.ts +12 -0
- package/dist/watchers/filesystem.d.ts.map +1 -0
- package/dist/watchers/filesystem.js +211 -0
- package/dist/watchers/filesystem.js.map +1 -0
- package/dist/watchers/github.d.ts +3 -0
- package/dist/watchers/github.d.ts.map +1 -0
- package/dist/watchers/github.js +322 -0
- package/dist/watchers/github.js.map +1 -0
- package/dist/watchers/http.d.ts +13 -0
- package/dist/watchers/http.d.ts.map +1 -0
- package/dist/watchers/http.js +149 -0
- package/dist/watchers/http.js.map +1 -0
- package/dist/watchers/index.d.ts +8 -0
- package/dist/watchers/index.d.ts.map +1 -0
- package/dist/watchers/index.js +25 -0
- package/dist/watchers/index.js.map +1 -0
- package/examples/README.md +9 -0
- package/examples/codebase-guardian.yaml +22 -0
- package/examples/competitor-watch.yaml +18 -0
- package/examples/founder-sentinel.md +126 -0
- package/examples/founder-sentinel.yaml +27 -0
- package/package.json +62 -0
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import axios from 'axios';
|
|
2
|
+
import { createHash } from 'crypto';
|
|
3
|
+
import fs from 'fs-extra';
|
|
4
|
+
import { join } from 'path';
|
|
5
|
+
import { homedir } from 'os';
|
|
6
|
+
function stateFilePath(url) {
|
|
7
|
+
const safe = url.replace(/[^a-zA-Z0-9]/g, '_').slice(0, 80);
|
|
8
|
+
return join(homedir(), '.demon', 'watcher-state', `http_${safe}.json`);
|
|
9
|
+
}
|
|
10
|
+
function hashBody(data) {
|
|
11
|
+
const str = typeof data === 'string' ? data : JSON.stringify(data);
|
|
12
|
+
return createHash('sha256').update(str).digest('hex');
|
|
13
|
+
}
|
|
14
|
+
function diffSummary(prev, next) {
|
|
15
|
+
if (!prev)
|
|
16
|
+
return 'Response content changed (first baseline set)';
|
|
17
|
+
return `Response hash changed: ${prev.slice(0, 8)} → ${next.slice(0, 8)}`;
|
|
18
|
+
}
|
|
19
|
+
export class HttpWatcher {
|
|
20
|
+
type = 'http';
|
|
21
|
+
config;
|
|
22
|
+
stateFile;
|
|
23
|
+
state = {
|
|
24
|
+
lastHash: null,
|
|
25
|
+
lastStatusCode: null,
|
|
26
|
+
lastCheckedAt: null,
|
|
27
|
+
};
|
|
28
|
+
stateLoaded = false;
|
|
29
|
+
constructor(config) {
|
|
30
|
+
this.config = config;
|
|
31
|
+
this.stateFile = stateFilePath(config.url ?? 'unknown');
|
|
32
|
+
}
|
|
33
|
+
async loadState() {
|
|
34
|
+
if (this.stateLoaded)
|
|
35
|
+
return;
|
|
36
|
+
try {
|
|
37
|
+
await fs.ensureDir(join(homedir(), '.demon', 'watcher-state'));
|
|
38
|
+
if (await fs.pathExists(this.stateFile)) {
|
|
39
|
+
this.state = await fs.readJson(this.stateFile);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
// Start fresh if state is corrupt
|
|
44
|
+
}
|
|
45
|
+
this.stateLoaded = true;
|
|
46
|
+
}
|
|
47
|
+
async saveState() {
|
|
48
|
+
try {
|
|
49
|
+
await fs.writeJson(this.stateFile, this.state, { spaces: 2 });
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
// Non-fatal — worst case we re-baseline next cycle
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
async collect() {
|
|
56
|
+
const { url, headers } = this.config;
|
|
57
|
+
if (!url)
|
|
58
|
+
throw new Error('http watcher requires a "url" field');
|
|
59
|
+
await this.loadState();
|
|
60
|
+
const startTime = Date.now();
|
|
61
|
+
let responseData;
|
|
62
|
+
let statusCode;
|
|
63
|
+
let error;
|
|
64
|
+
try {
|
|
65
|
+
const response = await axios.get(url, {
|
|
66
|
+
headers: headers ?? {},
|
|
67
|
+
timeout: 30_000,
|
|
68
|
+
validateStatus: () => true,
|
|
69
|
+
});
|
|
70
|
+
statusCode = response.status;
|
|
71
|
+
responseData = response.data;
|
|
72
|
+
}
|
|
73
|
+
catch (err) {
|
|
74
|
+
statusCode = 0;
|
|
75
|
+
error = err.message;
|
|
76
|
+
}
|
|
77
|
+
const latencyMs = Date.now() - startTime;
|
|
78
|
+
const isError = statusCode >= 400 || statusCode === 0;
|
|
79
|
+
const currentHash = error ? null : hashBody(responseData);
|
|
80
|
+
const contentChanged = currentHash !== null && currentHash !== this.state.lastHash;
|
|
81
|
+
const statusChanged = this.state.lastStatusCode !== null && statusCode !== this.state.lastStatusCode;
|
|
82
|
+
// Update persisted state
|
|
83
|
+
const prevHash = this.state.lastHash;
|
|
84
|
+
this.state = {
|
|
85
|
+
lastHash: currentHash ?? this.state.lastHash,
|
|
86
|
+
lastStatusCode: statusCode,
|
|
87
|
+
lastCheckedAt: new Date().toISOString(),
|
|
88
|
+
};
|
|
89
|
+
await this.saveState();
|
|
90
|
+
// Only emit an observation when something is notable
|
|
91
|
+
const shouldReport = isError || contentChanged || statusChanged || prevHash === null;
|
|
92
|
+
if (!shouldReport) {
|
|
93
|
+
return []; // No change, no error — skip this cycle
|
|
94
|
+
}
|
|
95
|
+
let summary;
|
|
96
|
+
if (error) {
|
|
97
|
+
summary = `HTTP GET ${url} failed: ${error}`;
|
|
98
|
+
}
|
|
99
|
+
else if (isError) {
|
|
100
|
+
summary = `HTTP GET ${url} → ${statusCode} ERROR (${latencyMs}ms)`;
|
|
101
|
+
}
|
|
102
|
+
else if (prevHash === null) {
|
|
103
|
+
summary = `HTTP GET ${url} → ${statusCode} OK (${latencyMs}ms) — baseline recorded`;
|
|
104
|
+
}
|
|
105
|
+
else if (statusChanged) {
|
|
106
|
+
summary = `HTTP GET ${url} status changed: ${this.state.lastStatusCode} → ${statusCode} (${latencyMs}ms)`;
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
summary =
|
|
110
|
+
`HTTP GET ${url} → ${statusCode} OK (${latencyMs}ms) — ` +
|
|
111
|
+
diffSummary(prevHash, currentHash);
|
|
112
|
+
}
|
|
113
|
+
return [
|
|
114
|
+
{
|
|
115
|
+
watcherType: this.type,
|
|
116
|
+
data: {
|
|
117
|
+
url,
|
|
118
|
+
statusCode,
|
|
119
|
+
latencyMs,
|
|
120
|
+
isOk: !isError,
|
|
121
|
+
isError,
|
|
122
|
+
contentChanged,
|
|
123
|
+
statusChanged,
|
|
124
|
+
currentHash: currentHash?.slice(0, 16) ?? null,
|
|
125
|
+
previousHash: prevHash?.slice(0, 16) ?? null,
|
|
126
|
+
error,
|
|
127
|
+
body: error ? undefined : truncateBody(responseData),
|
|
128
|
+
},
|
|
129
|
+
summary,
|
|
130
|
+
timestamp: new Date().toISOString(),
|
|
131
|
+
},
|
|
132
|
+
];
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
function truncateBody(data) {
|
|
136
|
+
if (typeof data === 'string') {
|
|
137
|
+
return data.length > 2000 ? data.slice(0, 2000) + '... [truncated]' : data;
|
|
138
|
+
}
|
|
139
|
+
if (typeof data === 'object' && data !== null) {
|
|
140
|
+
const json = JSON.stringify(data);
|
|
141
|
+
if (json.length > 2000) {
|
|
142
|
+
// Return a truncated string representation rather than broken JSON
|
|
143
|
+
return json.slice(0, 2000) + '... [truncated]';
|
|
144
|
+
}
|
|
145
|
+
return data;
|
|
146
|
+
}
|
|
147
|
+
return data;
|
|
148
|
+
}
|
|
149
|
+
//# sourceMappingURL=http.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"http.js","sourceRoot":"","sources":["../../src/watchers/http.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { Watcher, WatcherConfig } from '../types.js';
|
|
2
|
+
export { createWatcher } from './base.js';
|
|
3
|
+
/**
|
|
4
|
+
* buildWatcher — convenience factory used by the daemon loop.
|
|
5
|
+
* Credentials are resolved inside each watcher from env/secrets/providers.
|
|
6
|
+
*/
|
|
7
|
+
export declare function buildWatcher(config: WatcherConfig): Watcher;
|
|
8
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/watchers/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA;AAKpD,OAAO,EAAE,aAAa,EAAE,MAAM,WAAW,CAAA;AAEzC;;;GAGG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,aAAa,GAAG,OAAO,CAe3D"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { buildGitHubWatcher } from './github.js';
|
|
2
|
+
import { HttpWatcher } from './http.js';
|
|
3
|
+
import { FilesystemWatcher } from './filesystem.js';
|
|
4
|
+
export { createWatcher } from './base.js';
|
|
5
|
+
/**
|
|
6
|
+
* buildWatcher — convenience factory used by the daemon loop.
|
|
7
|
+
* Credentials are resolved inside each watcher from env/secrets/providers.
|
|
8
|
+
*/
|
|
9
|
+
export function buildWatcher(config) {
|
|
10
|
+
switch (config.type) {
|
|
11
|
+
case 'github_prs':
|
|
12
|
+
case 'github_ci':
|
|
13
|
+
case 'github_commits':
|
|
14
|
+
return buildGitHubWatcher(config, {});
|
|
15
|
+
case 'http':
|
|
16
|
+
return new HttpWatcher(config);
|
|
17
|
+
case 'filesystem':
|
|
18
|
+
return new FilesystemWatcher(config);
|
|
19
|
+
default: {
|
|
20
|
+
const _exhaustive = config.type;
|
|
21
|
+
throw new Error(`Unknown watcher type: ${String(_exhaustive)}`);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/watchers/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAA;AAChD,OAAO,EAAE,WAAW,EAAE,MAAM,WAAW,CAAA;AACvC,OAAO,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAA;AAEnD,OAAO,EAAE,aAAa,EAAE,MAAM,WAAW,CAAA;AAEzC;;;GAGG;AACH,MAAM,UAAU,YAAY,CAAC,MAAqB;IAChD,QAAQ,MAAM,CAAC,IAAI,EAAE,CAAC;QACpB,KAAK,YAAY,CAAC;QAClB,KAAK,WAAW,CAAC;QACjB,KAAK,gBAAgB;YACnB,OAAO,kBAAkB,CAAC,MAAM,EAAE,EAAE,CAAC,CAAA;QACvC,KAAK,MAAM;YACT,OAAO,IAAI,WAAW,CAAC,MAAM,CAAC,CAAA;QAChC,KAAK,YAAY;YACf,OAAO,IAAI,iBAAiB,CAAC,MAAM,CAAC,CAAA;QACtC,OAAO,CAAC,CAAC,CAAC;YACR,MAAM,WAAW,GAAU,MAAM,CAAC,IAAI,CAAA;YACtC,MAAM,IAAI,KAAK,CAAC,yBAAyB,MAAM,CAAC,WAAW,CAAC,EAAE,CAAC,CAAA;QACjE,CAAC;IACH,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# Examples
|
|
2
|
+
|
|
3
|
+
Reference demons you can copy, fill in, and run.
|
|
4
|
+
|
|
5
|
+
| Example | Description |
|
|
6
|
+
|---|---|
|
|
7
|
+
| [codebase-guardian](./codebase-guardian.yaml) | Watches a repo's PRs and CI, flags coverage drops, large diffs, and obvious bugs before they merge |
|
|
8
|
+
| [competitor-watch](./competitor-watch.yaml) | Monitors competitor pages for pricing changes, new features, and announcements |
|
|
9
|
+
| [founder-sentinel](./founder-sentinel.yaml) | Watches all your active projects and surfaces stale PRs, broken builds, and degraded health — only when something actually warrants it ([guide](./founder-sentinel.md)) |
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
name: codebase-guardian
|
|
2
|
+
purpose: |
|
|
3
|
+
Keep this codebase healthy. Watch for PRs that drop test coverage,
|
|
4
|
+
introduce obvious bugs, or are too large to review properly. Flag
|
|
5
|
+
issues proactively before they merge. Track patterns over time.
|
|
6
|
+
watch:
|
|
7
|
+
- type: github_prs
|
|
8
|
+
repo: OWNER/REPO
|
|
9
|
+
- type: github_ci
|
|
10
|
+
repo: OWNER/REPO
|
|
11
|
+
interval: 15m
|
|
12
|
+
action_mode: dry_run
|
|
13
|
+
actions:
|
|
14
|
+
- provider: github
|
|
15
|
+
action: post_comment
|
|
16
|
+
on: pr
|
|
17
|
+
- provider: slack
|
|
18
|
+
action: send_message
|
|
19
|
+
channel: "#engineering"
|
|
20
|
+
- provider: log
|
|
21
|
+
action: info
|
|
22
|
+
model: claude-sonnet-4-6
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
name: competitor-watch
|
|
2
|
+
purpose: |
|
|
3
|
+
Monitor competitor websites for meaningful changes — new features,
|
|
4
|
+
pricing changes, product announcements. Surface only what matters.
|
|
5
|
+
watch:
|
|
6
|
+
- type: http
|
|
7
|
+
url: https://competitor.com/pricing
|
|
8
|
+
- type: http
|
|
9
|
+
url: https://competitor.com/changelog
|
|
10
|
+
interval: 1h
|
|
11
|
+
action_mode: dry_run
|
|
12
|
+
actions:
|
|
13
|
+
- provider: slack
|
|
14
|
+
action: send_message
|
|
15
|
+
channel: "#competitive-intel"
|
|
16
|
+
- provider: log
|
|
17
|
+
action: info
|
|
18
|
+
model: claude-sonnet-4-6
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# founder-sentinel
|
|
2
|
+
|
|
3
|
+
A demon that watches all your active projects simultaneously and surfaces only what actually needs your attention — stale PRs, broken builds, degraded health endpoints, blocked work. It stays quiet when things are fine.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## What it does
|
|
8
|
+
|
|
9
|
+
You run multiple projects. Checking each one manually means either constant context switching or things slipping through. founder-sentinel runs on a 30-minute cycle, looks across all of them, and decides what matters right now.
|
|
10
|
+
|
|
11
|
+
It does not alert on every CI run or every commit. It flags things that have crossed a threshold: a PR open for 48+ hours with no review, a health endpoint that just started returning errors, a build that's been red across three consecutive runs. The judgment about what matters lives in the model, not in a ruleset you had to write upfront.
|
|
12
|
+
|
|
13
|
+
Over time, as it accumulates memory across runs, it gets better at knowing what's noise for your specific projects and what actually needs you.
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Setup
|
|
18
|
+
|
|
19
|
+
**1. Copy the YAML and fill in your repos and URLs**
|
|
20
|
+
|
|
21
|
+
```yaml
|
|
22
|
+
watch:
|
|
23
|
+
- type: github_prs
|
|
24
|
+
repo: yourname/frontend-app
|
|
25
|
+
- type: github_ci
|
|
26
|
+
repo: yourname/frontend-app
|
|
27
|
+
- type: http
|
|
28
|
+
url: https://yourapp.com/health
|
|
29
|
+
- type: github_prs
|
|
30
|
+
repo: yourname/api-service
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Add as many `watch` entries as you have active projects. Each `type: http` entry should point to a health or status endpoint that returns a non-200 when something is wrong.
|
|
34
|
+
|
|
35
|
+
**2. Configure notifications**
|
|
36
|
+
|
|
37
|
+
The `notify` provider sends a system notification (macOS/Linux). If you want Slack instead, swap it:
|
|
38
|
+
|
|
39
|
+
```yaml
|
|
40
|
+
actions:
|
|
41
|
+
- provider: slack
|
|
42
|
+
action: send_message
|
|
43
|
+
channel: "#founder-alerts"
|
|
44
|
+
- provider: log
|
|
45
|
+
action: info
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Keep `provider: log` as a secondary action — it gives you a local audit trail.
|
|
49
|
+
|
|
50
|
+
**3. Start in dry_run**
|
|
51
|
+
|
|
52
|
+
Leave `action_mode: dry_run` for the first week. In this mode the demon runs its full analysis every 30 minutes but only logs what it would have done. Nothing gets sent. You can review the log to see whether its judgment matches yours.
|
|
53
|
+
|
|
54
|
+
**4. When you're confident, switch to auto**
|
|
55
|
+
|
|
56
|
+
```yaml
|
|
57
|
+
action_mode: auto
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
That's it. From here it acts on its own judgments.
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
## The first week
|
|
65
|
+
|
|
66
|
+
Run it in `dry_run` and check the log once a day. Look for two failure modes:
|
|
67
|
+
|
|
68
|
+
**Too noisy** — it's flagging things you don't care about. Tighten your `watch` list, or add context to the `purpose` field to tell it what *not* to flag. Example:
|
|
69
|
+
|
|
70
|
+
```yaml
|
|
71
|
+
purpose: |
|
|
72
|
+
Watch all active projects for things that genuinely need attention.
|
|
73
|
+
Flag stale PRs, blocked work, degraded health, and important changes —
|
|
74
|
+
but only when something actually warrants it. Learn what matters over time.
|
|
75
|
+
Surface the right thing at the right moment, not noise.
|
|
76
|
+
|
|
77
|
+
Do not flag PRs that are marked as draft. Do not flag CI failures
|
|
78
|
+
on feature branches — only flag main/master.
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
**Missing things** — it let something slip that you wish it had caught. Add that class of watch to your `watch` list, or describe the pattern in `purpose` so the model knows to look for it.
|
|
82
|
+
|
|
83
|
+
After a week of the log matching your judgment, switch to `auto`.
|
|
84
|
+
|
|
85
|
+
---
|
|
86
|
+
|
|
87
|
+
## Example notifications
|
|
88
|
+
|
|
89
|
+
What you will actually receive once it's running:
|
|
90
|
+
|
|
91
|
+
> **[frontend-app]** PR #47 "Refactor auth flow" has been open 3 days with no review activity. Last CI run passed. Tagging for your attention.
|
|
92
|
+
|
|
93
|
+
> **[api-service]** Build has failed on main for the past 4 runs (2h 15m). Last green commit: a3f92c1. Three commits since then, all from the same author.
|
|
94
|
+
|
|
95
|
+
> **[yourapp.com/health]** Health endpoint returned 503 twice in the last two cycles. Previous 48 hours were clean. May be worth checking.
|
|
96
|
+
|
|
97
|
+
> **[frontend-app]** PR #51 merged while CI was red. Build is now fixed but this pattern has happened twice this week.
|
|
98
|
+
|
|
99
|
+
Notifications are batched per cycle. If nothing warrants attention in a given 30-minute window, nothing is sent.
|
|
100
|
+
|
|
101
|
+
---
|
|
102
|
+
|
|
103
|
+
## How it gets smarter over time
|
|
104
|
+
|
|
105
|
+
Each run, the demon writes a summary of what it observed and what it flagged to its memory store. On subsequent runs, it has context about:
|
|
106
|
+
|
|
107
|
+
- Which repos tend to have noisy CI that you don't care about
|
|
108
|
+
- Which PRs have been intentionally parked (it stops re-flagging them)
|
|
109
|
+
- What your normal baseline looks like for each health endpoint (so it can detect deviation, not just failure)
|
|
110
|
+
- Patterns across projects — e.g., if deploys on one service consistently break the health check on another
|
|
111
|
+
|
|
112
|
+
You do not configure any of this. It accumulates from observation. The longer it runs, the more it knows about your specific situation.
|
|
113
|
+
|
|
114
|
+
---
|
|
115
|
+
|
|
116
|
+
## Adjusting the interval
|
|
117
|
+
|
|
118
|
+
30 minutes is a reasonable default for a founder context — frequent enough to catch things before they compound, infrequent enough not to burn API budget on noise.
|
|
119
|
+
|
|
120
|
+
If your projects are high-velocity (multiple deploys per day), drop it to `15m`. If you're in a slower phase, `1h` is fine.
|
|
121
|
+
|
|
122
|
+
```yaml
|
|
123
|
+
interval: 15m # high-velocity sprint
|
|
124
|
+
interval: 30m # default
|
|
125
|
+
interval: 1h # maintenance mode
|
|
126
|
+
```
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
name: founder-sentinel
|
|
2
|
+
purpose: |
|
|
3
|
+
Watch all active projects for things that genuinely need attention.
|
|
4
|
+
Flag stale PRs, blocked work, degraded health, and important changes —
|
|
5
|
+
but only when something actually warrants it. Learn what matters over time.
|
|
6
|
+
Surface the right thing at the right moment, not noise.
|
|
7
|
+
|
|
8
|
+
watch:
|
|
9
|
+
- type: github_prs
|
|
10
|
+
repo: OWNER/REPO1
|
|
11
|
+
- type: github_ci
|
|
12
|
+
repo: OWNER/REPO1
|
|
13
|
+
- type: http
|
|
14
|
+
url: https://YOUR-APP.com/health
|
|
15
|
+
- type: github_prs
|
|
16
|
+
repo: OWNER/REPO2
|
|
17
|
+
|
|
18
|
+
interval: 30m
|
|
19
|
+
action_mode: dry_run
|
|
20
|
+
|
|
21
|
+
actions:
|
|
22
|
+
- provider: notify
|
|
23
|
+
action: send
|
|
24
|
+
- provider: log
|
|
25
|
+
action: info
|
|
26
|
+
|
|
27
|
+
model: claude-sonnet-4-6
|
package/package.json
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@josharsh/demon-cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "AI-native daemon runtime — run persistent demons (the CLI host for @josharsh/demon)",
|
|
5
|
+
"keywords": ["ai", "agent", "daemon", "llm", "automation", "demon"],
|
|
6
|
+
"author": "Harsh <engineering@spotlyte.live>",
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "https://github.com/josharsh/demon"
|
|
11
|
+
},
|
|
12
|
+
"homepage": "https://demon.josharsh.com",
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/josharsh/demon/issues"
|
|
15
|
+
},
|
|
16
|
+
"engines": {
|
|
17
|
+
"node": ">=18.0.0"
|
|
18
|
+
},
|
|
19
|
+
"workspaces": ["packages/*"],
|
|
20
|
+
"files": ["dist", "README.md", "examples", "LICENSE"],
|
|
21
|
+
"type": "module",
|
|
22
|
+
"bin": { "demon": "./dist/index.js" },
|
|
23
|
+
"main": "./dist/index.js",
|
|
24
|
+
"scripts": {
|
|
25
|
+
"build": "tsc",
|
|
26
|
+
"build:core": "npm run build -w @josharsh/demon",
|
|
27
|
+
"build:site": "npm run build -w @josharsh/demon && cp -R packages/core/dist/. site/core/",
|
|
28
|
+
"serve:site": "python3 -m http.server 8765 --directory site",
|
|
29
|
+
"dev": "tsx src/index.ts",
|
|
30
|
+
"test": "vitest run",
|
|
31
|
+
"test:core": "npm run test -w @josharsh/demon",
|
|
32
|
+
"test:watch": "vitest",
|
|
33
|
+
"typecheck": "tsc --noEmit",
|
|
34
|
+
"lint": "eslint src --ext .ts",
|
|
35
|
+
"prepublishOnly": "npm run build && npm test"
|
|
36
|
+
},
|
|
37
|
+
"dependencies": {
|
|
38
|
+
"@josharsh/demon": "^0.1.0",
|
|
39
|
+
"@octokit/rest": "^21.0.0",
|
|
40
|
+
"axios": "^1.7.0",
|
|
41
|
+
"chalk": "^5.3.0",
|
|
42
|
+
"cli-table3": "^0.6.5",
|
|
43
|
+
"commander": "^12.0.0",
|
|
44
|
+
"fs-extra": "^11.2.0",
|
|
45
|
+
"inquirer": "^10.0.0",
|
|
46
|
+
"js-yaml": "^4.1.0",
|
|
47
|
+
"ms": "^2.1.3",
|
|
48
|
+
"ora": "^8.0.0",
|
|
49
|
+
"pretty-ms": "^9.0.0",
|
|
50
|
+
"winston": "^3.14.0"
|
|
51
|
+
},
|
|
52
|
+
"devDependencies": {
|
|
53
|
+
"@types/fs-extra": "^11.0.4",
|
|
54
|
+
"@types/inquirer": "^9.0.7",
|
|
55
|
+
"@types/js-yaml": "^4.0.9",
|
|
56
|
+
"@types/ms": "^0.7.34",
|
|
57
|
+
"@types/node": "^22.0.0",
|
|
58
|
+
"tsx": "^4.19.0",
|
|
59
|
+
"typescript": "^5.6.0",
|
|
60
|
+
"vitest": "^2.1.0"
|
|
61
|
+
}
|
|
62
|
+
}
|