pulseed 0.1.3 → 0.1.4
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/dist/chat/chat-history.d.ts +80 -0
- package/dist/chat/chat-history.d.ts.map +1 -0
- package/dist/chat/chat-history.js +70 -0
- package/dist/chat/chat-history.js.map +1 -0
- package/dist/chat/chat-runner.d.ts +46 -0
- package/dist/chat/chat-runner.d.ts.map +1 -0
- package/dist/chat/chat-runner.js +141 -0
- package/dist/chat/chat-runner.js.map +1 -0
- package/dist/chat/escalation.d.ts +30 -0
- package/dist/chat/escalation.d.ts.map +1 -0
- package/dist/chat/escalation.js +53 -0
- package/dist/chat/escalation.js.map +1 -0
- package/dist/cli/commands/chat.d.ts +3 -0
- package/dist/cli/commands/chat.d.ts.map +1 -0
- package/dist/cli/commands/chat.js +148 -0
- package/dist/cli/commands/chat.js.map +1 -0
- package/dist/cli/commands/daemon.d.ts.map +1 -1
- package/dist/cli/commands/daemon.js +77 -14
- package/dist/cli/commands/daemon.js.map +1 -1
- package/dist/cli/commands/doctor.d.ts +19 -0
- package/dist/cli/commands/doctor.d.ts.map +1 -0
- package/dist/cli/commands/doctor.js +203 -0
- package/dist/cli/commands/doctor.js.map +1 -0
- package/dist/cli/commands/goal-utils.d.ts.map +1 -1
- package/dist/cli/commands/goal-utils.js +1 -0
- package/dist/cli/commands/goal-utils.js.map +1 -1
- package/dist/cli/commands/install.d.ts +16 -0
- package/dist/cli/commands/install.d.ts.map +1 -0
- package/dist/cli/commands/install.js +153 -0
- package/dist/cli/commands/install.js.map +1 -0
- package/dist/cli/commands/logs.d.ts +2 -0
- package/dist/cli/commands/logs.d.ts.map +1 -0
- package/dist/cli/commands/logs.js +201 -0
- package/dist/cli/commands/logs.js.map +1 -0
- package/dist/cli/commands/notify.d.ts +2 -0
- package/dist/cli/commands/notify.d.ts.map +1 -0
- package/dist/cli/commands/notify.js +277 -0
- package/dist/cli/commands/notify.js.map +1 -0
- package/dist/cli/setup.d.ts.map +1 -1
- package/dist/cli/setup.js +14 -4
- package/dist/cli/setup.js.map +1 -1
- package/dist/cli/utils.d.ts.map +1 -1
- package/dist/cli/utils.js +10 -0
- package/dist/cli/utils.js.map +1 -1
- package/dist/cli-runner.d.ts.map +1 -1
- package/dist/cli-runner.js +43 -2
- package/dist/cli-runner.js.map +1 -1
- package/dist/core-loop.d.ts +1 -1
- package/dist/core-loop.d.ts.map +1 -1
- package/dist/core-loop.js +7 -3
- package/dist/core-loop.js.map +1 -1
- package/dist/drive/drive-system.d.ts.map +1 -1
- package/dist/drive/drive-system.js +11 -2
- package/dist/drive/drive-system.js.map +1 -1
- package/dist/execution/issue-context-fetcher.d.ts +19 -0
- package/dist/execution/issue-context-fetcher.d.ts.map +1 -0
- package/dist/execution/issue-context-fetcher.js +73 -0
- package/dist/execution/issue-context-fetcher.js.map +1 -0
- package/dist/execution/session-manager.d.ts.map +1 -1
- package/dist/execution/session-manager.js +3 -0
- package/dist/execution/session-manager.js.map +1 -1
- package/dist/execution/task-generation.d.ts.map +1 -1
- package/dist/execution/task-generation.js +8 -0
- package/dist/execution/task-generation.js.map +1 -1
- package/dist/execution/task-lifecycle.d.ts.map +1 -1
- package/dist/execution/task-lifecycle.js +4 -1
- package/dist/execution/task-lifecycle.js.map +1 -1
- package/dist/execution/task-prompt-builder.d.ts.map +1 -1
- package/dist/execution/task-prompt-builder.js +30 -1
- package/dist/execution/task-prompt-builder.js.map +1 -1
- package/dist/execution/task-verifier.d.ts.map +1 -1
- package/dist/execution/task-verifier.js +16 -2
- package/dist/execution/task-verifier.js.map +1 -1
- package/dist/goal/goal-refiner.js +1 -1
- package/dist/goal/goal-refiner.js.map +1 -1
- package/dist/goal/goal-tree-manager.d.ts.map +1 -1
- package/dist/goal/goal-tree-manager.js +6 -0
- package/dist/goal/goal-tree-manager.js.map +1 -1
- package/dist/goal/refiner-prompts.d.ts.map +1 -1
- package/dist/goal/refiner-prompts.js +5 -1
- package/dist/goal/refiner-prompts.js.map +1 -1
- package/dist/goal/tree-loop-orchestrator.d.ts +3 -1
- package/dist/goal/tree-loop-orchestrator.d.ts.map +1 -1
- package/dist/goal/tree-loop-orchestrator.js +4 -4
- package/dist/goal/tree-loop-orchestrator.js.map +1 -1
- package/dist/loop/core-loop-phases.d.ts +1 -1
- package/dist/loop/core-loop-phases.d.ts.map +1 -1
- package/dist/loop/core-loop-phases.js +9 -3
- package/dist/loop/core-loop-phases.js.map +1 -1
- package/dist/loop/tree-loop-runner.d.ts.map +1 -1
- package/dist/loop/tree-loop-runner.js +10 -7
- package/dist/loop/tree-loop-runner.js.map +1 -1
- package/dist/observation/context-provider.d.ts +10 -0
- package/dist/observation/context-provider.d.ts.map +1 -1
- package/dist/observation/context-provider.js +35 -0
- package/dist/observation/context-provider.js.map +1 -1
- package/dist/observation/observation-engine.d.ts.map +1 -1
- package/dist/observation/observation-engine.js +1 -0
- package/dist/observation/observation-engine.js.map +1 -1
- package/dist/observation/observation-llm.d.ts.map +1 -1
- package/dist/observation/observation-llm.js +5 -1
- package/dist/observation/observation-llm.js.map +1 -1
- package/dist/observation/workspace-context.d.ts.map +1 -1
- package/dist/observation/workspace-context.js +59 -12
- package/dist/observation/workspace-context.js.map +1 -1
- package/dist/runtime/hook-manager.d.ts.map +1 -1
- package/dist/runtime/hook-manager.js +7 -0
- package/dist/runtime/hook-manager.js.map +1 -1
- package/dist/tui/entry.d.ts.map +1 -1
- package/dist/tui/entry.js +19 -1
- package/dist/tui/entry.js.map +1 -1
- package/dist/types/daemon.js +1 -1
- package/dist/types/daemon.js.map +1 -1
- package/dist/types/goal-refiner.d.ts +3 -0
- package/dist/types/goal-refiner.d.ts.map +1 -1
- package/dist/types/goal-refiner.js +1 -0
- package/dist/types/goal-refiner.js.map +1 -1
- package/dist/types/mcp.d.ts +6 -6
- package/dist/types/session.d.ts +4 -4
- package/dist/types/session.d.ts.map +1 -1
- package/dist/types/session.js +1 -0
- package/dist/types/session.js.map +1 -1
- package/dist/utils/execFileNoThrow.d.ts +20 -0
- package/dist/utils/execFileNoThrow.d.ts.map +1 -0
- package/dist/utils/execFileNoThrow.js +33 -0
- package/dist/utils/execFileNoThrow.js.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
// ─── pulseed install / uninstall (macOS launchd integration) ───
|
|
2
|
+
import { parseArgs } from "node:util";
|
|
3
|
+
import * as fs from "node:fs";
|
|
4
|
+
import * as path from "node:path";
|
|
5
|
+
import * as os from "node:os";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
import { execFileNoThrow } from "../../utils/execFileNoThrow.js";
|
|
8
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
9
|
+
const __dirname = path.dirname(__filename);
|
|
10
|
+
const PLIST_LABEL = "com.pulseed.daemon";
|
|
11
|
+
const PLIST_PATH = path.join(os.homedir(), "Library", "LaunchAgents", `${PLIST_LABEL}.plist`);
|
|
12
|
+
/** Build the plist XML string from the given parameters. */
|
|
13
|
+
export function buildPlist(opts) {
|
|
14
|
+
const programArgs = [opts.nodePath, opts.cliRunnerPath, "start"];
|
|
15
|
+
for (const id of opts.goalIds) {
|
|
16
|
+
programArgs.push("--goal", id);
|
|
17
|
+
}
|
|
18
|
+
if (opts.configPath) {
|
|
19
|
+
programArgs.push("--config", opts.configPath);
|
|
20
|
+
}
|
|
21
|
+
if (opts.intervalMs !== undefined) {
|
|
22
|
+
programArgs.push("--check-interval-ms", String(opts.intervalMs));
|
|
23
|
+
}
|
|
24
|
+
const argEntries = programArgs
|
|
25
|
+
.map((a) => `\t\t<string>${escapeXml(a)}</string>`)
|
|
26
|
+
.join("\n");
|
|
27
|
+
const envEntries = [];
|
|
28
|
+
if (opts.envPath) {
|
|
29
|
+
envEntries.push(`\t\t<key>PATH</key>\n\t\t<string>${escapeXml(opts.envPath)}</string>`);
|
|
30
|
+
}
|
|
31
|
+
if (opts.pulseedHome) {
|
|
32
|
+
envEntries.push(`\t\t<key>PULSEED_HOME</key>\n\t\t<string>${escapeXml(opts.pulseedHome)}</string>`);
|
|
33
|
+
}
|
|
34
|
+
const envBlock = envEntries.length > 0
|
|
35
|
+
? `\t<key>EnvironmentVariables</key>\n\t<dict>\n${envEntries.join("\n")}\n\t</dict>\n`
|
|
36
|
+
: "";
|
|
37
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
38
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
39
|
+
<plist version="1.0">
|
|
40
|
+
<dict>
|
|
41
|
+
\t<key>Label</key>
|
|
42
|
+
\t<string>${PLIST_LABEL}</string>
|
|
43
|
+
\t<key>ProgramArguments</key>
|
|
44
|
+
\t<array>
|
|
45
|
+
${argEntries}
|
|
46
|
+
\t</array>
|
|
47
|
+
\t<key>RunAtLoad</key>
|
|
48
|
+
\t<true/>
|
|
49
|
+
\t<key>KeepAlive</key>
|
|
50
|
+
\t<true/>
|
|
51
|
+
\t<key>StandardOutPath</key>
|
|
52
|
+
\t<string>${escapeXml(opts.stdoutLog)}</string>
|
|
53
|
+
\t<key>StandardErrorPath</key>
|
|
54
|
+
\t<string>${escapeXml(opts.stderrLog)}</string>
|
|
55
|
+
\t<key>WorkingDirectory</key>
|
|
56
|
+
\t<string>${escapeXml(opts.workingDir)}</string>
|
|
57
|
+
${envBlock}</dict>
|
|
58
|
+
</plist>
|
|
59
|
+
`;
|
|
60
|
+
}
|
|
61
|
+
function escapeXml(s) {
|
|
62
|
+
return s
|
|
63
|
+
.replace(/&/g, "&")
|
|
64
|
+
.replace(/</g, "<")
|
|
65
|
+
.replace(/>/g, ">")
|
|
66
|
+
.replace(/"/g, """)
|
|
67
|
+
.replace(/'/g, "'");
|
|
68
|
+
}
|
|
69
|
+
export async function cmdInstall(args) {
|
|
70
|
+
if (process.platform !== "darwin") {
|
|
71
|
+
console.error("launchd is only supported on macOS");
|
|
72
|
+
return 1;
|
|
73
|
+
}
|
|
74
|
+
let values;
|
|
75
|
+
try {
|
|
76
|
+
({ values } = parseArgs({
|
|
77
|
+
args,
|
|
78
|
+
options: {
|
|
79
|
+
goal: { type: "string", multiple: true },
|
|
80
|
+
config: { type: "string" },
|
|
81
|
+
interval: { type: "string" },
|
|
82
|
+
},
|
|
83
|
+
strict: false,
|
|
84
|
+
}));
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
console.error("Failed to parse arguments");
|
|
88
|
+
return 1;
|
|
89
|
+
}
|
|
90
|
+
const goalIds = values.goal ?? [];
|
|
91
|
+
if (goalIds.length === 0) {
|
|
92
|
+
console.error("Error: at least one --goal is required");
|
|
93
|
+
return 1;
|
|
94
|
+
}
|
|
95
|
+
const intervalMs = values.interval !== undefined ? parseInt(values.interval, 10) : undefined;
|
|
96
|
+
if (intervalMs !== undefined && (isNaN(intervalMs) || intervalMs <= 0)) {
|
|
97
|
+
console.error("--interval must be a positive integer (milliseconds)");
|
|
98
|
+
return 1;
|
|
99
|
+
}
|
|
100
|
+
if (fs.existsSync(PLIST_PATH)) {
|
|
101
|
+
console.warn(`Warning: plist already exists at ${PLIST_PATH}, overwriting`);
|
|
102
|
+
}
|
|
103
|
+
const nodePath = process.execPath;
|
|
104
|
+
// This file is compiled to dist/cli/commands/install.js — go up two levels to dist/
|
|
105
|
+
const cliRunnerPath = path.resolve(__dirname, "../../cli-runner.js");
|
|
106
|
+
const home = os.homedir();
|
|
107
|
+
const logsDir = path.join(home, ".pulseed", "logs");
|
|
108
|
+
const stdoutLog = path.join(logsDir, "launchd-stdout.log");
|
|
109
|
+
const stderrLog = path.join(logsDir, "launchd-stderr.log");
|
|
110
|
+
const plistContent = buildPlist({
|
|
111
|
+
nodePath,
|
|
112
|
+
cliRunnerPath,
|
|
113
|
+
goalIds,
|
|
114
|
+
configPath: values.config,
|
|
115
|
+
intervalMs,
|
|
116
|
+
stdoutLog,
|
|
117
|
+
stderrLog,
|
|
118
|
+
workingDir: home,
|
|
119
|
+
envPath: process.env["PATH"],
|
|
120
|
+
pulseedHome: process.env["PULSEED_HOME"],
|
|
121
|
+
});
|
|
122
|
+
// Ensure LaunchAgents directory exists
|
|
123
|
+
fs.mkdirSync(path.dirname(PLIST_PATH), { recursive: true });
|
|
124
|
+
fs.writeFileSync(PLIST_PATH, plistContent, "utf8");
|
|
125
|
+
const result = await execFileNoThrow("launchctl", ["load", PLIST_PATH]);
|
|
126
|
+
if (result.exitCode !== 0) {
|
|
127
|
+
console.error(`Failed to load plist with launchctl: ${result.stderr.trim()}`);
|
|
128
|
+
return 1;
|
|
129
|
+
}
|
|
130
|
+
console.log(`PulSeed daemon installed at: ${PLIST_PATH}`);
|
|
131
|
+
console.log(`To check status: launchctl list ${PLIST_LABEL}`);
|
|
132
|
+
console.log(`Logs: ${stdoutLog} / ${stderrLog}`);
|
|
133
|
+
return 0;
|
|
134
|
+
}
|
|
135
|
+
export async function cmdUninstall(_args) {
|
|
136
|
+
if (process.platform !== "darwin") {
|
|
137
|
+
console.error("launchd is only supported on macOS");
|
|
138
|
+
return 1;
|
|
139
|
+
}
|
|
140
|
+
if (!fs.existsSync(PLIST_PATH)) {
|
|
141
|
+
console.log("Not installed");
|
|
142
|
+
return 1;
|
|
143
|
+
}
|
|
144
|
+
const result = await execFileNoThrow("launchctl", ["unload", PLIST_PATH]);
|
|
145
|
+
if (result.exitCode !== 0) {
|
|
146
|
+
// Warn but still proceed to remove the plist file
|
|
147
|
+
console.warn(`Warning: launchctl unload returned an error: ${result.stderr.trim()}`);
|
|
148
|
+
}
|
|
149
|
+
fs.unlinkSync(PLIST_PATH);
|
|
150
|
+
console.log(`PulSeed daemon uninstalled (removed ${PLIST_PATH})`);
|
|
151
|
+
return 0;
|
|
152
|
+
}
|
|
153
|
+
//# sourceMappingURL=install.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"install.js","sourceRoot":"","sources":["../../../src/cli/commands/install.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"logs.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/logs.ts"],"names":[],"mappings":"AAkJA,wBAAsB,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAuE7D"}
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
// ─── pulseed logs command ───
|
|
2
|
+
//
|
|
3
|
+
// Shows daemon log lines from ~/.pulseed/logs/pulseed.log
|
|
4
|
+
// Supports: --lines N, --level <level>, --follow/-f
|
|
5
|
+
import { parseArgs } from "node:util";
|
|
6
|
+
import * as fs from "node:fs";
|
|
7
|
+
import * as path from "node:path";
|
|
8
|
+
import { getLogsDir } from "../../utils/paths.js";
|
|
9
|
+
const LEVEL_ORDER = {
|
|
10
|
+
DEBUG: 0,
|
|
11
|
+
INFO: 1,
|
|
12
|
+
WARN: 2,
|
|
13
|
+
ERROR: 3,
|
|
14
|
+
};
|
|
15
|
+
const READ_CHUNK_SIZE = 64 * 1024; // 64KB
|
|
16
|
+
function parseLevel(raw) {
|
|
17
|
+
const upper = raw.toUpperCase();
|
|
18
|
+
return upper in LEVEL_ORDER ? upper : null;
|
|
19
|
+
}
|
|
20
|
+
function lineMatchesLevel(line, minLevel) {
|
|
21
|
+
// Log format: [<ISO timestamp>] [<LEVEL padded 5>] ...
|
|
22
|
+
// e.g. [2026-04-01T00:00:00.000Z] [ERROR] message
|
|
23
|
+
const match = line.match(/\[([A-Z ]{4,5})\]/);
|
|
24
|
+
if (!match || !match[1])
|
|
25
|
+
return false;
|
|
26
|
+
const found = match[1].trim();
|
|
27
|
+
if (!(found in LEVEL_ORDER))
|
|
28
|
+
return false;
|
|
29
|
+
return LEVEL_ORDER[found] >= LEVEL_ORDER[minLevel];
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Read the last N lines from a file efficiently by reading a chunk from the end.
|
|
33
|
+
*/
|
|
34
|
+
function readLastLines(filePath, n) {
|
|
35
|
+
let stat;
|
|
36
|
+
try {
|
|
37
|
+
stat = fs.statSync(filePath);
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return [];
|
|
41
|
+
}
|
|
42
|
+
if (stat.size === 0)
|
|
43
|
+
return [];
|
|
44
|
+
const readSize = Math.min(READ_CHUNK_SIZE, stat.size);
|
|
45
|
+
const buffer = Buffer.alloc(readSize);
|
|
46
|
+
const fd = fs.openSync(filePath, "r");
|
|
47
|
+
try {
|
|
48
|
+
fs.readSync(fd, buffer, 0, readSize, stat.size - readSize);
|
|
49
|
+
}
|
|
50
|
+
finally {
|
|
51
|
+
fs.closeSync(fd);
|
|
52
|
+
}
|
|
53
|
+
const content = buffer.toString("utf8");
|
|
54
|
+
const lines = content.split("\n");
|
|
55
|
+
// Remove trailing empty entry from trailing newline
|
|
56
|
+
if (lines[lines.length - 1] === "")
|
|
57
|
+
lines.pop();
|
|
58
|
+
return lines.slice(-n);
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Follow a log file, printing new lines as they are appended.
|
|
62
|
+
* Handles file rotation by re-opening when the file is replaced.
|
|
63
|
+
*/
|
|
64
|
+
async function followLog(filePath, minLevel) {
|
|
65
|
+
let fileSize = 0;
|
|
66
|
+
try {
|
|
67
|
+
fileSize = fs.statSync(filePath).size;
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
// File may not exist yet; start at 0
|
|
71
|
+
}
|
|
72
|
+
let watcher = null;
|
|
73
|
+
let stopped = false;
|
|
74
|
+
const checkForNewLines = () => {
|
|
75
|
+
try {
|
|
76
|
+
const stat = fs.statSync(filePath);
|
|
77
|
+
if (stat.size < fileSize) {
|
|
78
|
+
// File was rotated/truncated — reset position
|
|
79
|
+
fileSize = 0;
|
|
80
|
+
}
|
|
81
|
+
if (stat.size === fileSize)
|
|
82
|
+
return;
|
|
83
|
+
const toRead = stat.size - fileSize;
|
|
84
|
+
const buf = Buffer.alloc(toRead);
|
|
85
|
+
const fd = fs.openSync(filePath, "r");
|
|
86
|
+
try {
|
|
87
|
+
fs.readSync(fd, buf, 0, toRead, fileSize);
|
|
88
|
+
}
|
|
89
|
+
finally {
|
|
90
|
+
fs.closeSync(fd);
|
|
91
|
+
}
|
|
92
|
+
fileSize = stat.size;
|
|
93
|
+
const chunk = buf.toString("utf8");
|
|
94
|
+
const lines = chunk.split("\n");
|
|
95
|
+
// The last element may be an incomplete line if the write was partial;
|
|
96
|
+
// for simplicity, print complete lines only (those followed by \n)
|
|
97
|
+
for (let i = 0; i < lines.length - 1; i++) {
|
|
98
|
+
const line = lines[i];
|
|
99
|
+
if (minLevel === null || lineMatchesLevel(line, minLevel)) {
|
|
100
|
+
process.stdout.write(line + "\n");
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
// File disappeared (rotation in progress) — will retry on next event
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
// Initial drain in case file already has content
|
|
109
|
+
checkForNewLines();
|
|
110
|
+
const setupWatcher = () => {
|
|
111
|
+
try {
|
|
112
|
+
watcher = fs.watch(filePath, () => {
|
|
113
|
+
checkForNewLines();
|
|
114
|
+
});
|
|
115
|
+
watcher.on("error", () => {
|
|
116
|
+
// Watch target gone; re-try after a short delay
|
|
117
|
+
watcher = null;
|
|
118
|
+
if (!stopped)
|
|
119
|
+
setTimeout(setupWatcher, 500);
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
// File may not exist yet; retry
|
|
124
|
+
if (!stopped)
|
|
125
|
+
setTimeout(setupWatcher, 500);
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
setupWatcher();
|
|
129
|
+
await new Promise((resolve) => {
|
|
130
|
+
process.once("SIGINT", () => {
|
|
131
|
+
stopped = true;
|
|
132
|
+
watcher?.close();
|
|
133
|
+
resolve();
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
export async function cmdLogs(args) {
|
|
138
|
+
let values;
|
|
139
|
+
try {
|
|
140
|
+
({ values } = parseArgs({
|
|
141
|
+
args,
|
|
142
|
+
options: {
|
|
143
|
+
follow: { type: "boolean", short: "f" },
|
|
144
|
+
lines: { type: "string", short: "n", default: "50" },
|
|
145
|
+
level: { type: "string" },
|
|
146
|
+
},
|
|
147
|
+
strict: false,
|
|
148
|
+
}));
|
|
149
|
+
}
|
|
150
|
+
catch {
|
|
151
|
+
values = { lines: "50" };
|
|
152
|
+
}
|
|
153
|
+
const logPath = path.join(getLogsDir(), "pulseed.log");
|
|
154
|
+
const lineCount = parseInt(values.lines ?? "50", 10);
|
|
155
|
+
const n = isNaN(lineCount) || lineCount <= 0 ? 50 : lineCount;
|
|
156
|
+
let minLevel = null;
|
|
157
|
+
if (values.level) {
|
|
158
|
+
minLevel = parseLevel(values.level);
|
|
159
|
+
if (minLevel === null) {
|
|
160
|
+
process.stderr.write(`Unknown log level: "${values.level}". Valid levels: DEBUG, INFO, WARN, ERROR\n`);
|
|
161
|
+
return 1;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
// Check file existence for non-follow mode
|
|
165
|
+
if (!values.follow) {
|
|
166
|
+
try {
|
|
167
|
+
fs.accessSync(logPath, fs.constants.R_OK);
|
|
168
|
+
}
|
|
169
|
+
catch {
|
|
170
|
+
process.stdout.write(`No log file found at ${logPath}\n`);
|
|
171
|
+
return 1;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
if (values.follow) {
|
|
175
|
+
// For follow mode, print existing tail first, then watch
|
|
176
|
+
let existingLines = [];
|
|
177
|
+
try {
|
|
178
|
+
fs.accessSync(logPath, fs.constants.R_OK);
|
|
179
|
+
existingLines = readLastLines(logPath, n);
|
|
180
|
+
}
|
|
181
|
+
catch {
|
|
182
|
+
// File not yet present; that's fine for follow mode
|
|
183
|
+
}
|
|
184
|
+
for (const line of existingLines) {
|
|
185
|
+
if (minLevel === null || lineMatchesLevel(line, minLevel)) {
|
|
186
|
+
process.stdout.write(line + "\n");
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
await followLog(logPath, minLevel);
|
|
190
|
+
return 0;
|
|
191
|
+
}
|
|
192
|
+
// Non-follow: read last N lines and print
|
|
193
|
+
const lines = readLastLines(logPath, n);
|
|
194
|
+
for (const line of lines) {
|
|
195
|
+
if (minLevel === null || lineMatchesLevel(line, minLevel)) {
|
|
196
|
+
process.stdout.write(line + "\n");
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return 0;
|
|
200
|
+
}
|
|
201
|
+
//# sourceMappingURL=logs.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"logs.js","sourceRoot":"","sources":["../../../src/cli/commands/logs.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"notify.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/notify.ts"],"names":[],"mappings":"AAgSA,wBAAsB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CA8B/D"}
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
// ─── pulseed notify commands (add, list, remove, test) ───
|
|
2
|
+
import { parseArgs } from "node:util";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import { readJsonFileOrNull, writeJsonFileAtomic } from "../../utils/json-io.js";
|
|
5
|
+
import { NotificationConfigSchema } from "../../types/notification.js";
|
|
6
|
+
import { getPulseedDirPath } from "../../utils/paths.js";
|
|
7
|
+
function getNotificationConfigPath(baseDir) {
|
|
8
|
+
return path.join(baseDir ?? getPulseedDirPath(), "notification.json");
|
|
9
|
+
}
|
|
10
|
+
async function loadConfig(configPath) {
|
|
11
|
+
const raw = await readJsonFileOrNull(configPath);
|
|
12
|
+
if (raw === null) {
|
|
13
|
+
return NotificationConfigSchema.parse({});
|
|
14
|
+
}
|
|
15
|
+
const result = NotificationConfigSchema.safeParse(raw);
|
|
16
|
+
if (!result.success) {
|
|
17
|
+
console.error(`Warning: notification config has invalid format, resetting. (${result.error.message})`);
|
|
18
|
+
return NotificationConfigSchema.parse({});
|
|
19
|
+
}
|
|
20
|
+
return result.data;
|
|
21
|
+
}
|
|
22
|
+
async function saveConfig(configPath, config) {
|
|
23
|
+
await writeJsonFileAtomic(configPath, config);
|
|
24
|
+
}
|
|
25
|
+
function formatChannel(ch, index) {
|
|
26
|
+
let detail = "";
|
|
27
|
+
if (ch.type === "slack") {
|
|
28
|
+
detail = `webhook: ${ch.webhook_url}`;
|
|
29
|
+
}
|
|
30
|
+
else if (ch.type === "webhook") {
|
|
31
|
+
detail = `url: ${ch.url}`;
|
|
32
|
+
}
|
|
33
|
+
else if (ch.type === "email") {
|
|
34
|
+
detail = `address: ${ch.address}, smtp: ${ch.smtp.host}:${ch.smtp.port}`;
|
|
35
|
+
}
|
|
36
|
+
const reports = ch.report_types.length > 0 ? ` (reports: ${ch.report_types.join(", ")})` : "";
|
|
37
|
+
return `[${index}] ${ch.type.padEnd(7)} — ${detail}${reports}`;
|
|
38
|
+
}
|
|
39
|
+
async function cmdNotifyAdd(args, configPath) {
|
|
40
|
+
const positionals = args.filter((a) => !a.startsWith("-"));
|
|
41
|
+
const channelType = positionals[0];
|
|
42
|
+
if (!channelType) {
|
|
43
|
+
console.error("Usage: pulseed notify add <slack|webhook|email> [options]");
|
|
44
|
+
return 1;
|
|
45
|
+
}
|
|
46
|
+
if (channelType === "slack") {
|
|
47
|
+
let values;
|
|
48
|
+
try {
|
|
49
|
+
({ values } = parseArgs({
|
|
50
|
+
args,
|
|
51
|
+
options: {
|
|
52
|
+
"webhook-url": { type: "string" },
|
|
53
|
+
},
|
|
54
|
+
strict: false,
|
|
55
|
+
}));
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
values = {};
|
|
59
|
+
}
|
|
60
|
+
if (!values["webhook-url"]) {
|
|
61
|
+
console.error("Error: --webhook-url is required for slack channel");
|
|
62
|
+
return 1;
|
|
63
|
+
}
|
|
64
|
+
const config = await loadConfig(configPath);
|
|
65
|
+
const channel = {
|
|
66
|
+
type: "slack",
|
|
67
|
+
webhook_url: values["webhook-url"],
|
|
68
|
+
report_types: [],
|
|
69
|
+
format: "compact",
|
|
70
|
+
};
|
|
71
|
+
config.channels.push(channel);
|
|
72
|
+
await saveConfig(configPath, config);
|
|
73
|
+
console.log(`Added slack channel (index ${config.channels.length - 1})`);
|
|
74
|
+
return 0;
|
|
75
|
+
}
|
|
76
|
+
if (channelType === "webhook") {
|
|
77
|
+
let values;
|
|
78
|
+
try {
|
|
79
|
+
({ values } = parseArgs({
|
|
80
|
+
args,
|
|
81
|
+
options: {
|
|
82
|
+
url: { type: "string" },
|
|
83
|
+
header: { type: "string", multiple: true },
|
|
84
|
+
},
|
|
85
|
+
strict: false,
|
|
86
|
+
}));
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
values = {};
|
|
90
|
+
}
|
|
91
|
+
if (!values.url) {
|
|
92
|
+
console.error("Error: --url is required for webhook channel");
|
|
93
|
+
return 1;
|
|
94
|
+
}
|
|
95
|
+
const headers = {};
|
|
96
|
+
for (const h of values.header ?? []) {
|
|
97
|
+
const colonIdx = h.indexOf(":");
|
|
98
|
+
if (colonIdx === -1) {
|
|
99
|
+
console.error(`Error: invalid header format "${h}", expected "Key: Value"`);
|
|
100
|
+
return 1;
|
|
101
|
+
}
|
|
102
|
+
const key = h.slice(0, colonIdx).trim();
|
|
103
|
+
const val = h.slice(colonIdx + 1).trim();
|
|
104
|
+
headers[key] = val;
|
|
105
|
+
}
|
|
106
|
+
const config = await loadConfig(configPath);
|
|
107
|
+
const channel = {
|
|
108
|
+
type: "webhook",
|
|
109
|
+
url: values.url,
|
|
110
|
+
report_types: [],
|
|
111
|
+
format: "json",
|
|
112
|
+
...(Object.keys(headers).length > 0 ? { headers } : {}),
|
|
113
|
+
};
|
|
114
|
+
config.channels.push(channel);
|
|
115
|
+
await saveConfig(configPath, config);
|
|
116
|
+
console.log(`Added webhook channel (index ${config.channels.length - 1})`);
|
|
117
|
+
return 0;
|
|
118
|
+
}
|
|
119
|
+
if (channelType === "email") {
|
|
120
|
+
let values;
|
|
121
|
+
try {
|
|
122
|
+
({ values } = parseArgs({
|
|
123
|
+
args,
|
|
124
|
+
options: {
|
|
125
|
+
address: { type: "string" },
|
|
126
|
+
"smtp-host": { type: "string" },
|
|
127
|
+
"smtp-port": { type: "string" },
|
|
128
|
+
},
|
|
129
|
+
strict: false,
|
|
130
|
+
}));
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
values = {};
|
|
134
|
+
}
|
|
135
|
+
if (!values.address) {
|
|
136
|
+
console.error("Error: --address is required for email channel");
|
|
137
|
+
return 1;
|
|
138
|
+
}
|
|
139
|
+
if (!values["smtp-host"]) {
|
|
140
|
+
console.error("Error: --smtp-host is required for email channel");
|
|
141
|
+
return 1;
|
|
142
|
+
}
|
|
143
|
+
const smtpPort = values["smtp-port"] ? parseInt(values["smtp-port"], 10) : 587;
|
|
144
|
+
if (isNaN(smtpPort) || smtpPort <= 0) {
|
|
145
|
+
console.error("Error: --smtp-port must be a positive integer");
|
|
146
|
+
return 1;
|
|
147
|
+
}
|
|
148
|
+
const config = await loadConfig(configPath);
|
|
149
|
+
const channel = {
|
|
150
|
+
type: "email",
|
|
151
|
+
address: values.address,
|
|
152
|
+
smtp: {
|
|
153
|
+
host: values["smtp-host"],
|
|
154
|
+
port: smtpPort,
|
|
155
|
+
secure: true,
|
|
156
|
+
auth: { user: "", pass: "" },
|
|
157
|
+
},
|
|
158
|
+
report_types: [],
|
|
159
|
+
format: "full",
|
|
160
|
+
};
|
|
161
|
+
config.channels.push(channel);
|
|
162
|
+
await saveConfig(configPath, config);
|
|
163
|
+
console.log(`Added email channel (index ${config.channels.length - 1})`);
|
|
164
|
+
return 0;
|
|
165
|
+
}
|
|
166
|
+
console.error(`Error: unknown channel type "${channelType}". Use: slack, webhook, email`);
|
|
167
|
+
return 1;
|
|
168
|
+
}
|
|
169
|
+
async function cmdNotifyList(configPath) {
|
|
170
|
+
const config = await loadConfig(configPath);
|
|
171
|
+
if (config.channels.length === 0) {
|
|
172
|
+
console.log("No channels configured");
|
|
173
|
+
return 0;
|
|
174
|
+
}
|
|
175
|
+
console.log("Notification Channels:");
|
|
176
|
+
for (let i = 0; i < config.channels.length; i++) {
|
|
177
|
+
console.log(formatChannel(config.channels[i], i));
|
|
178
|
+
}
|
|
179
|
+
return 0;
|
|
180
|
+
}
|
|
181
|
+
async function cmdNotifyRemove(args, configPath) {
|
|
182
|
+
const indexStr = args[0];
|
|
183
|
+
if (!indexStr) {
|
|
184
|
+
console.error("Usage: pulseed notify remove <index>");
|
|
185
|
+
return 1;
|
|
186
|
+
}
|
|
187
|
+
const index = parseInt(indexStr, 10);
|
|
188
|
+
if (isNaN(index) || index < 0) {
|
|
189
|
+
console.error("Error: index must be a non-negative integer");
|
|
190
|
+
return 1;
|
|
191
|
+
}
|
|
192
|
+
const config = await loadConfig(configPath);
|
|
193
|
+
if (index >= config.channels.length) {
|
|
194
|
+
console.error(`Error: index ${index} out of bounds (${config.channels.length} channel(s) configured)`);
|
|
195
|
+
return 1;
|
|
196
|
+
}
|
|
197
|
+
const removed = config.channels.splice(index, 1)[0];
|
|
198
|
+
await saveConfig(configPath, config);
|
|
199
|
+
console.log(`Removed ${removed.type} channel at index ${index}`);
|
|
200
|
+
return 0;
|
|
201
|
+
}
|
|
202
|
+
async function cmdNotifyTest(args, configPath) {
|
|
203
|
+
const config = await loadConfig(configPath);
|
|
204
|
+
if (config.channels.length === 0) {
|
|
205
|
+
console.log("No channels configured");
|
|
206
|
+
return 0;
|
|
207
|
+
}
|
|
208
|
+
const indexStr = args[0];
|
|
209
|
+
let targets;
|
|
210
|
+
if (indexStr !== undefined) {
|
|
211
|
+
const index = parseInt(indexStr, 10);
|
|
212
|
+
if (isNaN(index) || index < 0) {
|
|
213
|
+
console.error("Error: index must be a non-negative integer");
|
|
214
|
+
return 1;
|
|
215
|
+
}
|
|
216
|
+
if (index >= config.channels.length) {
|
|
217
|
+
console.error(`Error: index ${index} out of bounds (${config.channels.length} channel(s) configured)`);
|
|
218
|
+
return 1;
|
|
219
|
+
}
|
|
220
|
+
targets = [{ index, channel: config.channels[index] }];
|
|
221
|
+
}
|
|
222
|
+
else {
|
|
223
|
+
targets = config.channels.map((ch, i) => ({ index: i, channel: ch }));
|
|
224
|
+
}
|
|
225
|
+
const testPayload = {
|
|
226
|
+
type: "test",
|
|
227
|
+
message: "PulSeed notification test",
|
|
228
|
+
timestamp: new Date().toISOString(),
|
|
229
|
+
};
|
|
230
|
+
console.log("Test notification payload (dry-run — daemon must be running for actual delivery):");
|
|
231
|
+
console.log(JSON.stringify(testPayload, null, 2));
|
|
232
|
+
console.log("");
|
|
233
|
+
for (const { index, channel } of targets) {
|
|
234
|
+
console.log(`[${index}] ${channel.type} — would send to:`);
|
|
235
|
+
if (channel.type === "slack") {
|
|
236
|
+
console.log(` webhook_url: ${channel.webhook_url}`);
|
|
237
|
+
}
|
|
238
|
+
else if (channel.type === "webhook") {
|
|
239
|
+
console.log(` url: ${channel.url}`);
|
|
240
|
+
if (channel.headers && Object.keys(channel.headers).length > 0) {
|
|
241
|
+
for (const [k, v] of Object.entries(channel.headers)) {
|
|
242
|
+
console.log(` header: ${k}: ${v}`);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
else if (channel.type === "email") {
|
|
247
|
+
console.log(` address: ${channel.address}`);
|
|
248
|
+
console.log(` smtp: ${channel.smtp.host}:${channel.smtp.port}`);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
return 0;
|
|
252
|
+
}
|
|
253
|
+
export async function cmdNotify(args) {
|
|
254
|
+
const subcommand = args[0];
|
|
255
|
+
const rest = args.slice(1);
|
|
256
|
+
const configPath = getNotificationConfigPath();
|
|
257
|
+
switch (subcommand) {
|
|
258
|
+
case "add":
|
|
259
|
+
return cmdNotifyAdd(rest, configPath);
|
|
260
|
+
case "list":
|
|
261
|
+
return cmdNotifyList(configPath);
|
|
262
|
+
case "remove":
|
|
263
|
+
return cmdNotifyRemove(rest, configPath);
|
|
264
|
+
case "test":
|
|
265
|
+
return cmdNotifyTest(rest, configPath);
|
|
266
|
+
default:
|
|
267
|
+
console.error("Usage: pulseed notify <add|list|remove|test>\n" +
|
|
268
|
+
" add slack --webhook-url <url>\n" +
|
|
269
|
+
" add webhook --url <url> [--header 'Key: Value']\n" +
|
|
270
|
+
" add email --address <email> --smtp-host <host> [--smtp-port <port>]\n" +
|
|
271
|
+
" list\n" +
|
|
272
|
+
" remove <index>\n" +
|
|
273
|
+
" test [index]");
|
|
274
|
+
return 1;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
//# sourceMappingURL=notify.js.map
|