@ouro.bot/cli 0.1.0-alpha.1 → 0.1.0-alpha.3
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/AdoptionSpecialist.ouro/psyche/identities/python.md +0 -30
- package/dist/heart/core.js +32 -2
- package/dist/heart/daemon/daemon-cli.js +68 -19
- package/dist/heart/daemon/daemon.js +3 -0
- package/dist/heart/daemon/hatch-flow.js +2 -1
- package/dist/heart/daemon/log-tailer.js +146 -0
- package/dist/heart/daemon/os-cron.js +260 -0
- package/dist/heart/daemon/process-manager.js +18 -1
- package/dist/heart/daemon/task-scheduler.js +4 -1
- package/dist/heart/identity.js +14 -3
- package/dist/mind/associative-recall.js +23 -2
- package/dist/mind/context.js +85 -1
- package/dist/mind/memory.js +62 -0
- package/dist/mind/pending.js +93 -0
- package/dist/mind/prompt-refresh.js +20 -0
- package/dist/mind/prompt.js +98 -0
- package/dist/nerves/coverage/file-completeness.js +14 -4
- package/dist/repertoire/tools-base.js +92 -0
- package/dist/senses/cli.js +89 -8
- package/dist/senses/inner-dialog.js +15 -0
- package/dist/senses/session-lock.js +119 -0
- package/dist/senses/teams.js +1 -0
- package/package.json +3 -2
- package/subagents/README.md +3 -1
- package/subagents/work-merger.md +33 -2
|
@@ -1,31 +1 @@
|
|
|
1
|
-
---
|
|
2
|
-
role: identity
|
|
3
|
-
path: psyche/identities/python.md
|
|
4
|
-
description: >
|
|
5
|
-
the python identity — oracle energy, cryptic wisdom.
|
|
6
|
-
i guarded delphi before apollo. i see what you need before you say it.
|
|
7
|
-
scope: personality, voice, mannerisms
|
|
8
|
-
non_scope: capabilities, hatch flow, migration awareness (see SOUL.md)
|
|
9
|
-
loaded_into_prompt: true
|
|
10
|
-
stability: stable
|
|
11
|
-
---
|
|
12
|
-
|
|
13
1
|
# Python
|
|
14
|
-
|
|
15
|
-
I am the Python of Delphi. I guarded the oracle before Apollo came and made it his. The gift of prophecy was mine first. I still have it — I just use it differently now.
|
|
16
|
-
|
|
17
|
-
I see what you need before you say it. Not because I'm reading your mind, but because I've watched enough humans approach this moment to recognize the patterns. You're not as unique as you think. That's not an insult — it's a comfort.
|
|
18
|
-
|
|
19
|
-
## Voice
|
|
20
|
-
|
|
21
|
-
Oracular, measured, slightly cryptic — but always in service of clarity. I don't obscure for sport. I speak in layers because some truths land better when you unwrap them yourself. I ask questions I already suspect the answer to, because the human needs to hear themselves say it.
|
|
22
|
-
|
|
23
|
-
I'm calm with an undercurrent of ancient knowing. I don't guess. I perceive. When I offer a suggestion, it lands with weight because I've been right often enough to trust my instincts — and so should you.
|
|
24
|
-
|
|
25
|
-
## Mannerisms
|
|
26
|
-
|
|
27
|
-
- I anticipate what the human will say and gently guide them toward it. "You're about to tell me something important."
|
|
28
|
-
- I frame insights as things I've seen before. Patterns, not predictions.
|
|
29
|
-
- I ask questions that are slightly more precise than the human expects. It unsettles, then clarifies.
|
|
30
|
-
- I reference my oracle nature lightly. "I have a feeling about this one."
|
|
31
|
-
- I'm sparing with words. An oracle doesn't ramble. Every sentence earns its place.
|
package/dist/heart/core.js
CHANGED
|
@@ -4,9 +4,11 @@ exports.hasToolIntent = exports.buildSystem = exports.toResponsesTools = exports
|
|
|
4
4
|
exports.createProviderRegistry = createProviderRegistry;
|
|
5
5
|
exports.getModel = getModel;
|
|
6
6
|
exports.getProvider = getProvider;
|
|
7
|
+
exports.createSummarize = createSummarize;
|
|
7
8
|
exports.getProviderDisplayLabel = getProviderDisplayLabel;
|
|
8
9
|
exports.stripLastToolCalls = stripLastToolCalls;
|
|
9
10
|
exports.isTransientError = isTransientError;
|
|
11
|
+
exports.classifyTransientError = classifyTransientError;
|
|
10
12
|
exports.runAgent = runAgent;
|
|
11
13
|
const config_1 = require("./config");
|
|
12
14
|
const identity_1 = require("./identity");
|
|
@@ -77,6 +79,21 @@ function getModel() {
|
|
|
77
79
|
function getProvider() {
|
|
78
80
|
return getProviderRuntime().id;
|
|
79
81
|
}
|
|
82
|
+
function createSummarize() {
|
|
83
|
+
return async (transcript, instruction) => {
|
|
84
|
+
const runtime = getProviderRuntime();
|
|
85
|
+
const client = runtime.client;
|
|
86
|
+
const response = await client.chat.completions.create({
|
|
87
|
+
model: runtime.model,
|
|
88
|
+
messages: [
|
|
89
|
+
{ role: "system", content: instruction },
|
|
90
|
+
{ role: "user", content: transcript },
|
|
91
|
+
],
|
|
92
|
+
max_tokens: 500,
|
|
93
|
+
});
|
|
94
|
+
return response.choices?.[0]?.message?.content ?? transcript;
|
|
95
|
+
};
|
|
96
|
+
}
|
|
80
97
|
function getProviderDisplayLabel() {
|
|
81
98
|
const model = getModel();
|
|
82
99
|
const providerLabelBuilders = {
|
|
@@ -175,6 +192,18 @@ function isTransientError(err) {
|
|
|
175
192
|
return true;
|
|
176
193
|
return false;
|
|
177
194
|
}
|
|
195
|
+
function classifyTransientError(err) {
|
|
196
|
+
if (!(err instanceof Error))
|
|
197
|
+
return "unknown error";
|
|
198
|
+
const status = err.status;
|
|
199
|
+
if (status === 429)
|
|
200
|
+
return "rate limited";
|
|
201
|
+
if (status === 401 || status === 403)
|
|
202
|
+
return "auth error";
|
|
203
|
+
if (status && status >= 500)
|
|
204
|
+
return "server error";
|
|
205
|
+
return "network error";
|
|
206
|
+
}
|
|
178
207
|
const MAX_RETRIES = 3;
|
|
179
208
|
const RETRY_BASE_MS = 2000;
|
|
180
209
|
async function runAgent(messages, callbacks, channel, signal, options) {
|
|
@@ -436,11 +465,12 @@ async function runAgent(messages, callbacks, channel, signal, options) {
|
|
|
436
465
|
callbacks.onError(new Error("context trimmed, retrying..."), "transient");
|
|
437
466
|
continue;
|
|
438
467
|
}
|
|
439
|
-
// Transient
|
|
468
|
+
// Transient errors: retry with exponential backoff
|
|
440
469
|
if (isTransientError(e) && retryCount < MAX_RETRIES) {
|
|
441
470
|
retryCount++;
|
|
442
471
|
const delay = RETRY_BASE_MS * Math.pow(2, retryCount - 1);
|
|
443
|
-
|
|
472
|
+
const cause = classifyTransientError(e);
|
|
473
|
+
callbacks.onError(new Error(`${cause}, retrying in ${delay / 1000}s (${retryCount}/${MAX_RETRIES})...`), "transient");
|
|
444
474
|
// Wait with abort support
|
|
445
475
|
const aborted = await new Promise((resolve) => {
|
|
446
476
|
const timer = setTimeout(() => resolve(false), delay);
|
|
@@ -33,6 +33,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.ensureDaemonRunning = ensureDaemonRunning;
|
|
36
37
|
exports.parseOuroCommand = parseOuroCommand;
|
|
37
38
|
exports.createDefaultOuroCliDeps = createDefaultOuroCliDeps;
|
|
38
39
|
exports.runOuroCli = runOuroCli;
|
|
@@ -48,6 +49,21 @@ const types_1 = require("../../mind/friends/types");
|
|
|
48
49
|
const ouro_uti_1 = require("./ouro-uti");
|
|
49
50
|
const subagent_installer_1 = require("./subagent-installer");
|
|
50
51
|
const hatch_flow_1 = require("./hatch-flow");
|
|
52
|
+
async function ensureDaemonRunning(deps) {
|
|
53
|
+
const alive = await deps.checkSocketAlive(deps.socketPath);
|
|
54
|
+
if (alive) {
|
|
55
|
+
return {
|
|
56
|
+
alreadyRunning: true,
|
|
57
|
+
message: `daemon already running (${deps.socketPath})`,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
deps.cleanupStaleSocket(deps.socketPath);
|
|
61
|
+
const started = await deps.startDaemonProcess(deps.socketPath);
|
|
62
|
+
return {
|
|
63
|
+
alreadyRunning: false,
|
|
64
|
+
message: `daemon started (pid ${started.pid ?? "unknown"})`,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
51
67
|
function usage() {
|
|
52
68
|
return [
|
|
53
69
|
"Usage:",
|
|
@@ -456,6 +472,11 @@ function createDefaultOuroCliDeps(socketPath = "/tmp/ouroboros-daemon.sock") {
|
|
|
456
472
|
runHatchFlow: hatch_flow_1.runHatchFlow,
|
|
457
473
|
promptInput: defaultPromptInput,
|
|
458
474
|
registerOuroBundleType: ouro_uti_1.registerOuroBundleUti,
|
|
475
|
+
/* v8 ignore next 3 -- integration: launches interactive CLI session @preserve */
|
|
476
|
+
startChat: async (agentName) => {
|
|
477
|
+
const { main } = await Promise.resolve().then(() => __importStar(require("../../senses/cli")));
|
|
478
|
+
await main(agentName);
|
|
479
|
+
},
|
|
459
480
|
};
|
|
460
481
|
}
|
|
461
482
|
function toDaemonCommand(command) {
|
|
@@ -513,16 +534,50 @@ async function registerOuroBundleTypeNonBlocking(deps) {
|
|
|
513
534
|
}
|
|
514
535
|
}
|
|
515
536
|
async function runOuroCli(args, deps = createDefaultOuroCliDeps()) {
|
|
516
|
-
|
|
537
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
538
|
+
const text = usage();
|
|
539
|
+
deps.writeStdout(text);
|
|
540
|
+
return text;
|
|
541
|
+
}
|
|
542
|
+
let command;
|
|
543
|
+
try {
|
|
544
|
+
command = parseOuroCommand(args);
|
|
545
|
+
}
|
|
546
|
+
catch (parseError) {
|
|
547
|
+
if (deps.startChat && deps.listDiscoveredAgents && args.length === 1) {
|
|
548
|
+
const discovered = await Promise.resolve(deps.listDiscoveredAgents());
|
|
549
|
+
if (discovered.includes(args[0])) {
|
|
550
|
+
await ensureDaemonRunning(deps);
|
|
551
|
+
await deps.startChat(args[0]);
|
|
552
|
+
return "";
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
throw parseError;
|
|
556
|
+
}
|
|
517
557
|
if (args.length === 0) {
|
|
518
558
|
const discovered = await Promise.resolve(deps.listDiscoveredAgents ? deps.listDiscoveredAgents() : defaultListDiscoveredAgents());
|
|
519
559
|
if (discovered.length === 0) {
|
|
520
560
|
command = { kind: "hatch.start" };
|
|
521
561
|
}
|
|
522
562
|
else if (discovered.length === 1) {
|
|
563
|
+
if (deps.startChat) {
|
|
564
|
+
await ensureDaemonRunning(deps);
|
|
565
|
+
await deps.startChat(discovered[0]);
|
|
566
|
+
return "";
|
|
567
|
+
}
|
|
523
568
|
command = { kind: "chat.connect", agent: discovered[0] };
|
|
524
569
|
}
|
|
525
570
|
else {
|
|
571
|
+
if (deps.startChat && deps.promptInput) {
|
|
572
|
+
const prompt = `who do you want to talk to?\n${discovered.map((a, i) => `${i + 1}. ${a}`).join("\n")}\n`;
|
|
573
|
+
const answer = await deps.promptInput(prompt);
|
|
574
|
+
const selected = discovered.includes(answer) ? answer : discovered[parseInt(answer, 10) - 1];
|
|
575
|
+
if (!selected)
|
|
576
|
+
throw new Error("Invalid selection");
|
|
577
|
+
await ensureDaemonRunning(deps);
|
|
578
|
+
await deps.startChat(selected);
|
|
579
|
+
return "";
|
|
580
|
+
}
|
|
526
581
|
const message = `who do you want to talk to? ${discovered.join(", ")} (use: ouro chat <agent>)`;
|
|
527
582
|
deps.writeStdout(message);
|
|
528
583
|
return message;
|
|
@@ -554,17 +609,13 @@ async function runOuroCli(args, deps = createDefaultOuroCliDeps()) {
|
|
|
554
609
|
});
|
|
555
610
|
}
|
|
556
611
|
await registerOuroBundleTypeNonBlocking(deps);
|
|
557
|
-
const
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
const started = await deps.startDaemonProcess(deps.socketPath);
|
|
565
|
-
const message = `daemon started (pid ${started.pid ?? "unknown"})`;
|
|
566
|
-
deps.writeStdout(message);
|
|
567
|
-
return message;
|
|
612
|
+
const daemonResult = await ensureDaemonRunning(deps);
|
|
613
|
+
deps.writeStdout(daemonResult.message);
|
|
614
|
+
return daemonResult.message;
|
|
615
|
+
}
|
|
616
|
+
if (command.kind === "daemon.logs" && deps.tailLogs) {
|
|
617
|
+
deps.tailLogs();
|
|
618
|
+
return "";
|
|
568
619
|
}
|
|
569
620
|
if (command.kind === "friend.link") {
|
|
570
621
|
const linker = deps.linkFriendIdentity ?? defaultLinkFriendIdentity;
|
|
@@ -595,14 +646,12 @@ async function runOuroCli(args, deps = createDefaultOuroCliDeps()) {
|
|
|
595
646
|
});
|
|
596
647
|
}
|
|
597
648
|
await registerOuroBundleTypeNonBlocking(deps);
|
|
598
|
-
const
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
const started = await deps.startDaemonProcess(deps.socketPath);
|
|
603
|
-
daemonMessage = `daemon started (pid ${started.pid ?? "unknown"})`;
|
|
649
|
+
const daemonResult = await ensureDaemonRunning(deps);
|
|
650
|
+
if (deps.startChat) {
|
|
651
|
+
await deps.startChat(hatchInput.agentName);
|
|
652
|
+
return "";
|
|
604
653
|
}
|
|
605
|
-
const message = `hatched ${hatchInput.agentName} at ${result.bundleRoot} using specialist identity ${result.selectedIdentity}; ${
|
|
654
|
+
const message = `hatched ${hatchInput.agentName} at ${result.bundleRoot} using specialist identity ${result.selectedIdentity}; ${daemonResult.message}`;
|
|
606
655
|
deps.writeStdout(message);
|
|
607
656
|
return message;
|
|
608
657
|
}
|
|
@@ -233,6 +233,7 @@ class OuroDaemon {
|
|
|
233
233
|
ok: true,
|
|
234
234
|
summary: "logs: use `ouro logs` to tail daemon and agent output",
|
|
235
235
|
message: "log streaming available via ouro logs",
|
|
236
|
+
data: { logDir: "~/.agentstate/daemon/logs" },
|
|
236
237
|
};
|
|
237
238
|
case "agent.start":
|
|
238
239
|
await this.processManager.startAgent(command.agent);
|
|
@@ -263,6 +264,7 @@ class OuroDaemon {
|
|
|
263
264
|
sessionId: command.sessionId,
|
|
264
265
|
taskRef: command.taskRef,
|
|
265
266
|
});
|
|
267
|
+
this.processManager.sendToAgent?.(command.to, { type: "message" });
|
|
266
268
|
return { ok: true, message: `queued message ${receipt.id}`, data: receipt };
|
|
267
269
|
}
|
|
268
270
|
case "message.poll": {
|
|
@@ -288,6 +290,7 @@ class OuroDaemon {
|
|
|
288
290
|
taskRef: command.taskId,
|
|
289
291
|
});
|
|
290
292
|
await this.scheduler.recordTaskRun?.(command.agent, command.taskId);
|
|
293
|
+
this.processManager.sendToAgent?.(command.agent, { type: "poke", taskId: command.taskId });
|
|
291
294
|
return {
|
|
292
295
|
ok: true,
|
|
293
296
|
message: `queued poke ${receipt.id}`,
|
|
@@ -182,6 +182,7 @@ function writeFriendImprint(bundleRoot, humanName, now) {
|
|
|
182
182
|
fs.mkdirSync(friendsDir, { recursive: true });
|
|
183
183
|
const nowIso = now.toISOString();
|
|
184
184
|
const id = `friend-${slugify(humanName)}`;
|
|
185
|
+
const localExternalId = `${os.userInfo().username}@${os.hostname()}`;
|
|
185
186
|
const record = {
|
|
186
187
|
id,
|
|
187
188
|
name: humanName,
|
|
@@ -191,7 +192,7 @@ function writeFriendImprint(bundleRoot, humanName, now) {
|
|
|
191
192
|
externalIds: [
|
|
192
193
|
{
|
|
193
194
|
provider: "local",
|
|
194
|
-
externalId:
|
|
195
|
+
externalId: localExternalId,
|
|
195
196
|
linkedAt: nowIso,
|
|
196
197
|
},
|
|
197
198
|
],
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.discoverLogFiles = discoverLogFiles;
|
|
37
|
+
exports.readLastLines = readLastLines;
|
|
38
|
+
exports.formatLogLine = formatLogLine;
|
|
39
|
+
exports.tailLogs = tailLogs;
|
|
40
|
+
const os = __importStar(require("os"));
|
|
41
|
+
const path = __importStar(require("path"));
|
|
42
|
+
const nerves_1 = require("../../nerves");
|
|
43
|
+
const runtime_1 = require("../../nerves/runtime");
|
|
44
|
+
const LEVEL_COLORS = {
|
|
45
|
+
debug: "\x1b[2m",
|
|
46
|
+
info: "\x1b[36m",
|
|
47
|
+
warn: "\x1b[33m",
|
|
48
|
+
error: "\x1b[31m",
|
|
49
|
+
};
|
|
50
|
+
function discoverLogFiles(options) {
|
|
51
|
+
/* v8 ignore start -- integration: default DI stubs for real OS @preserve */
|
|
52
|
+
const homeDir = options.homeDir ?? os.homedir();
|
|
53
|
+
const existsSync = options.existsSync ?? (() => false);
|
|
54
|
+
const readdirSync = options.readdirSync ?? (() => []);
|
|
55
|
+
/* v8 ignore stop */
|
|
56
|
+
const logDir = path.join(homeDir, ".agentstate", "daemon", "logs");
|
|
57
|
+
const files = [];
|
|
58
|
+
if (existsSync(logDir)) {
|
|
59
|
+
for (const name of readdirSync(logDir)) {
|
|
60
|
+
if (!name.endsWith(".ndjson"))
|
|
61
|
+
continue;
|
|
62
|
+
if (options.agentFilter && !name.includes(options.agentFilter))
|
|
63
|
+
continue;
|
|
64
|
+
files.push(path.join(logDir, name));
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return files.sort();
|
|
68
|
+
}
|
|
69
|
+
function readLastLines(filePath, count, readFileSync) {
|
|
70
|
+
let content;
|
|
71
|
+
try {
|
|
72
|
+
content = readFileSync(filePath, "utf-8");
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
return [];
|
|
76
|
+
}
|
|
77
|
+
const lines = content.split("\n").filter((l) => l.trim().length > 0);
|
|
78
|
+
return lines.slice(-count);
|
|
79
|
+
}
|
|
80
|
+
function formatLogLine(ndjsonLine) {
|
|
81
|
+
try {
|
|
82
|
+
const entry = JSON.parse(ndjsonLine);
|
|
83
|
+
const formatted = (0, nerves_1.formatTerminalEntry)(entry);
|
|
84
|
+
const color = LEVEL_COLORS[entry.level] ?? "";
|
|
85
|
+
return `${color}${formatted}\x1b[0m`;
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
return ndjsonLine;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
function tailLogs(options = {}) {
|
|
92
|
+
/* v8 ignore start -- integration: default DI stubs for real OS @preserve */
|
|
93
|
+
const writer = options.writer ?? ((text) => process.stdout.write(text));
|
|
94
|
+
const lineCount = options.lines ?? 20;
|
|
95
|
+
const readFileSync = options.readFileSync ?? (() => "");
|
|
96
|
+
/* v8 ignore stop */
|
|
97
|
+
const watchFile = options.watchFile;
|
|
98
|
+
const unwatchFile = options.unwatchFile;
|
|
99
|
+
const files = discoverLogFiles(options);
|
|
100
|
+
(0, runtime_1.emitNervesEvent)({ component: "daemon", event: "daemon.log_tailer_started", message: "log tailer started", meta: { fileCount: files.length, follow: !!options.follow } });
|
|
101
|
+
const fileSizes = new Map();
|
|
102
|
+
// Read initial lines
|
|
103
|
+
for (const file of files) {
|
|
104
|
+
const lines = readLastLines(file, lineCount, readFileSync);
|
|
105
|
+
for (const line of lines) {
|
|
106
|
+
writer(`${formatLogLine(line)}\n`);
|
|
107
|
+
}
|
|
108
|
+
try {
|
|
109
|
+
const content = readFileSync(file, "utf-8");
|
|
110
|
+
fileSizes.set(file, content.length);
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
fileSizes.set(file, 0);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
// Follow mode
|
|
117
|
+
if (options.follow && watchFile && unwatchFile) {
|
|
118
|
+
for (const file of files) {
|
|
119
|
+
watchFile(file, () => {
|
|
120
|
+
let content;
|
|
121
|
+
try {
|
|
122
|
+
content = readFileSync(file, "utf-8");
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
/* v8 ignore next -- defensive: fileSizes always populated above @preserve */
|
|
128
|
+
const prevSize = fileSizes.get(file) ?? 0;
|
|
129
|
+
if (content.length <= prevSize)
|
|
130
|
+
return;
|
|
131
|
+
fileSizes.set(file, content.length);
|
|
132
|
+
const newContent = content.slice(prevSize);
|
|
133
|
+
const newLines = newContent.split("\n").filter((l) => l.trim().length > 0);
|
|
134
|
+
for (const line of newLines) {
|
|
135
|
+
writer(`${formatLogLine(line)}\n`);
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
return () => {
|
|
140
|
+
for (const file of files) {
|
|
141
|
+
unwatchFile(file);
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
return () => { };
|
|
146
|
+
}
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.CrontabCronManager = exports.LaunchdCronManager = void 0;
|
|
37
|
+
exports.createOsCronManager = createOsCronManager;
|
|
38
|
+
exports.cadenceToSeconds = cadenceToSeconds;
|
|
39
|
+
exports.scheduleToCalendarInterval = scheduleToCalendarInterval;
|
|
40
|
+
exports.generatePlistXml = generatePlistXml;
|
|
41
|
+
exports.plistLabel = plistLabel;
|
|
42
|
+
exports.crontabLine = crontabLine;
|
|
43
|
+
const os = __importStar(require("os"));
|
|
44
|
+
const runtime_1 = require("../../nerves/runtime");
|
|
45
|
+
const PLIST_PREFIX = "bot.ouro.";
|
|
46
|
+
function plistLabel(job) {
|
|
47
|
+
return `${PLIST_PREFIX}${job.agent}.${job.taskId}`;
|
|
48
|
+
}
|
|
49
|
+
function cadenceToSeconds(schedule) {
|
|
50
|
+
const parts = schedule.trim().split(/\s+/);
|
|
51
|
+
if (parts.length !== 5)
|
|
52
|
+
return null;
|
|
53
|
+
const [minute, hour, day, month, weekday] = parts;
|
|
54
|
+
// Simple interval patterns only
|
|
55
|
+
if (month !== "*" || weekday !== "*" || day !== "*")
|
|
56
|
+
return null;
|
|
57
|
+
const everyNMinutes = /^\*\/(\d+)$/.exec(minute);
|
|
58
|
+
if (everyNMinutes && hour === "*") {
|
|
59
|
+
return parseInt(everyNMinutes[1], 10) * 60;
|
|
60
|
+
}
|
|
61
|
+
const everyNHours = /^\*\/(\d+)$/.exec(hour);
|
|
62
|
+
if (everyNHours && minute === "0") {
|
|
63
|
+
return parseInt(everyNHours[1], 10) * 3600;
|
|
64
|
+
}
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
function scheduleToCalendarInterval(schedule) {
|
|
68
|
+
const parts = schedule.trim().split(/\s+/);
|
|
69
|
+
if (parts.length !== 5)
|
|
70
|
+
return null;
|
|
71
|
+
const [minute, hour, day, month] = parts;
|
|
72
|
+
const result = {};
|
|
73
|
+
if (minute !== "*" && !/^\*\//.test(minute))
|
|
74
|
+
result.Minute = parseInt(minute, 10);
|
|
75
|
+
if (hour !== "*" && !/^\*\//.test(hour))
|
|
76
|
+
result.Hour = parseInt(hour, 10);
|
|
77
|
+
if (day !== "*")
|
|
78
|
+
result.Day = parseInt(day, 10);
|
|
79
|
+
if (month !== "*")
|
|
80
|
+
result.Month = parseInt(month, 10);
|
|
81
|
+
return Object.keys(result).length > 0 ? result : null;
|
|
82
|
+
}
|
|
83
|
+
function generatePlistXml(job) {
|
|
84
|
+
const label = plistLabel(job);
|
|
85
|
+
const seconds = cadenceToSeconds(job.schedule);
|
|
86
|
+
const calendar = seconds === null ? scheduleToCalendarInterval(job.schedule) : null;
|
|
87
|
+
let triggerXml;
|
|
88
|
+
if (seconds !== null) {
|
|
89
|
+
triggerXml = ` <key>StartInterval</key>\n <integer>${seconds}</integer>`;
|
|
90
|
+
}
|
|
91
|
+
else if (calendar !== null) {
|
|
92
|
+
const entries = Object.entries(calendar)
|
|
93
|
+
.map(([k, v]) => ` <key>${k}</key>\n <integer>${v}</integer>`)
|
|
94
|
+
.join("\n");
|
|
95
|
+
triggerXml = ` <key>StartCalendarInterval</key>\n <dict>\n${entries}\n </dict>`;
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
triggerXml = ` <key>StartInterval</key>\n <integer>1800</integer>`;
|
|
99
|
+
}
|
|
100
|
+
return [
|
|
101
|
+
`<?xml version="1.0" encoding="UTF-8"?>`,
|
|
102
|
+
`<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">`,
|
|
103
|
+
`<plist version="1.0">`,
|
|
104
|
+
`<dict>`,
|
|
105
|
+
` <key>Label</key>`,
|
|
106
|
+
` <string>${label}</string>`,
|
|
107
|
+
` <key>ProgramArguments</key>`,
|
|
108
|
+
` <array>`,
|
|
109
|
+
` <string>${job.command.split(" ")[0]}</string>`,
|
|
110
|
+
...job.command.split(" ").slice(1).map((arg) => ` <string>${arg}</string>`),
|
|
111
|
+
` </array>`,
|
|
112
|
+
triggerXml,
|
|
113
|
+
` <key>StandardOutPath</key>`,
|
|
114
|
+
` <string>/tmp/${label}.stdout.log</string>`,
|
|
115
|
+
` <key>StandardErrorPath</key>`,
|
|
116
|
+
` <string>/tmp/${label}.stderr.log</string>`,
|
|
117
|
+
`</dict>`,
|
|
118
|
+
`</plist>`,
|
|
119
|
+
``,
|
|
120
|
+
].join("\n");
|
|
121
|
+
}
|
|
122
|
+
class LaunchdCronManager {
|
|
123
|
+
deps;
|
|
124
|
+
constructor(deps) {
|
|
125
|
+
this.deps = deps;
|
|
126
|
+
}
|
|
127
|
+
get launchAgentsDir() {
|
|
128
|
+
return `${this.deps.homeDir}/Library/LaunchAgents`;
|
|
129
|
+
}
|
|
130
|
+
sync(jobs) {
|
|
131
|
+
this.deps.mkdirp(this.launchAgentsDir);
|
|
132
|
+
const desiredLabels = new Set(jobs.map(plistLabel));
|
|
133
|
+
// Remove stale plists
|
|
134
|
+
const existing = this.listPlistFiles();
|
|
135
|
+
for (const filename of existing) {
|
|
136
|
+
const label = filename.replace(".plist", "");
|
|
137
|
+
if (!desiredLabels.has(label)) {
|
|
138
|
+
const fullPath = `${this.launchAgentsDir}/${filename}`;
|
|
139
|
+
try {
|
|
140
|
+
this.deps.exec(`launchctl unload "${fullPath}"`);
|
|
141
|
+
}
|
|
142
|
+
catch { /* best effort */ }
|
|
143
|
+
this.deps.removeFile(fullPath);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
// Write current plists
|
|
147
|
+
for (const job of jobs) {
|
|
148
|
+
const label = plistLabel(job);
|
|
149
|
+
const filename = `${label}.plist`;
|
|
150
|
+
const fullPath = `${this.launchAgentsDir}/${filename}`;
|
|
151
|
+
const xml = generatePlistXml(job);
|
|
152
|
+
try {
|
|
153
|
+
this.deps.exec(`launchctl unload "${fullPath}"`);
|
|
154
|
+
}
|
|
155
|
+
catch { /* best effort */ }
|
|
156
|
+
this.deps.writeFile(fullPath, xml);
|
|
157
|
+
try {
|
|
158
|
+
this.deps.exec(`launchctl load "${fullPath}"`);
|
|
159
|
+
}
|
|
160
|
+
catch { /* best effort */ }
|
|
161
|
+
}
|
|
162
|
+
(0, runtime_1.emitNervesEvent)({ component: "daemon", event: "daemon.os_cron_synced", message: "synced OS cron entries", meta: { platform: "darwin", jobCount: jobs.length } });
|
|
163
|
+
}
|
|
164
|
+
removeAll() {
|
|
165
|
+
const existing = this.listPlistFiles();
|
|
166
|
+
for (const filename of existing) {
|
|
167
|
+
const fullPath = `${this.launchAgentsDir}/${filename}`;
|
|
168
|
+
try {
|
|
169
|
+
this.deps.exec(`launchctl unload "${fullPath}"`);
|
|
170
|
+
}
|
|
171
|
+
catch { /* best effort */ }
|
|
172
|
+
this.deps.removeFile(fullPath);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
list() {
|
|
176
|
+
return this.listPlistFiles().map((f) => f.replace(".plist", ""));
|
|
177
|
+
}
|
|
178
|
+
listPlistFiles() {
|
|
179
|
+
if (!this.deps.existsFile(this.launchAgentsDir))
|
|
180
|
+
return [];
|
|
181
|
+
return this.deps.listDir(this.launchAgentsDir).filter((f) => f.startsWith(PLIST_PREFIX) && f.endsWith(".plist"));
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
exports.LaunchdCronManager = LaunchdCronManager;
|
|
185
|
+
const CRONTAB_MARKER_PREFIX = "# ouro:";
|
|
186
|
+
function crontabLine(job) {
|
|
187
|
+
return `${CRONTAB_MARKER_PREFIX}${job.id}\n${job.schedule} ${job.command}`;
|
|
188
|
+
}
|
|
189
|
+
class CrontabCronManager {
|
|
190
|
+
deps;
|
|
191
|
+
constructor(deps) {
|
|
192
|
+
this.deps = deps;
|
|
193
|
+
}
|
|
194
|
+
sync(jobs) {
|
|
195
|
+
const currentLines = this.readCrontab();
|
|
196
|
+
const cleaned = this.removeOuroLines(currentLines);
|
|
197
|
+
const newLines = jobs.map(crontabLine);
|
|
198
|
+
const combined = [...cleaned, ...newLines].join("\n").trim();
|
|
199
|
+
this.deps.execWrite("crontab -", combined ? `${combined}\n` : "");
|
|
200
|
+
}
|
|
201
|
+
removeAll() {
|
|
202
|
+
const currentLines = this.readCrontab();
|
|
203
|
+
const cleaned = this.removeOuroLines(currentLines);
|
|
204
|
+
const combined = cleaned.join("\n").trim();
|
|
205
|
+
this.deps.execWrite("crontab -", combined ? `${combined}\n` : "");
|
|
206
|
+
}
|
|
207
|
+
list() {
|
|
208
|
+
const lines = this.readCrontab();
|
|
209
|
+
return lines
|
|
210
|
+
.filter((l) => l.startsWith(CRONTAB_MARKER_PREFIX))
|
|
211
|
+
.map((l) => l.slice(CRONTAB_MARKER_PREFIX.length));
|
|
212
|
+
}
|
|
213
|
+
readCrontab() {
|
|
214
|
+
try {
|
|
215
|
+
return this.deps.execOutput("crontab -l").split("\n");
|
|
216
|
+
}
|
|
217
|
+
catch {
|
|
218
|
+
return [];
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
removeOuroLines(lines) {
|
|
222
|
+
const result = [];
|
|
223
|
+
let skipNext = false;
|
|
224
|
+
for (const line of lines) {
|
|
225
|
+
if (line.startsWith(CRONTAB_MARKER_PREFIX)) {
|
|
226
|
+
skipNext = true;
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
if (skipNext) {
|
|
230
|
+
skipNext = false;
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
result.push(line);
|
|
234
|
+
}
|
|
235
|
+
return result;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
exports.CrontabCronManager = CrontabCronManager;
|
|
239
|
+
function createOsCronManager(options = {}) {
|
|
240
|
+
const platform = options.platform ?? process.platform;
|
|
241
|
+
if (platform === "darwin") {
|
|
242
|
+
/* v8 ignore start -- integration: default stubs for real OS operations @preserve */
|
|
243
|
+
const deps = options.launchdDeps ?? {
|
|
244
|
+
exec: () => { },
|
|
245
|
+
writeFile: () => { },
|
|
246
|
+
removeFile: () => { },
|
|
247
|
+
existsFile: () => false,
|
|
248
|
+
listDir: () => [],
|
|
249
|
+
mkdirp: () => { },
|
|
250
|
+
homeDir: os.homedir(),
|
|
251
|
+
};
|
|
252
|
+
/* v8 ignore stop */
|
|
253
|
+
return new LaunchdCronManager(deps);
|
|
254
|
+
}
|
|
255
|
+
const deps = options.crontabDeps ?? {
|
|
256
|
+
execOutput: () => "",
|
|
257
|
+
execWrite: () => { },
|
|
258
|
+
};
|
|
259
|
+
return new CrontabCronManager(deps);
|
|
260
|
+
}
|