macroclaw 0.2.0 → 0.4.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/src/index.ts CHANGED
@@ -1,58 +1,48 @@
1
1
  import { cpSync, existsSync, readdirSync } from "node:fs";
2
2
  import { dirname, join, resolve } from "node:path";
3
- import { createInterface } from "node:readline";
4
3
  import { App, type AppConfig } from "./app";
5
4
  import { createLogger, initLogger } from "./logger";
6
- import { applyEnvOverrides, loadSettings, printSettings, saveSettings } from "./settings";
7
- import { runSetupWizard } from "./setup";
5
+ import { applyEnvOverrides, loadSettings, printSettings } from "./settings";
8
6
 
9
- await initLogger();
10
- const log = createLogger("index");
7
+ export async function start(): Promise<void> {
8
+ const log = createLogger("index");
11
9
 
12
- const defaultDir = resolve(process.env.HOME || "~", ".macroclaw");
10
+ const defaultDir = resolve(process.env.HOME || "~", ".macroclaw");
13
11
 
14
- let settings = loadSettings(defaultDir);
12
+ const settings = loadSettings(defaultDir);
15
13
 
16
- if (!settings) {
17
- log.info("No settings.json found, starting setup wizard");
18
- const rl = createInterface({ input: process.stdin, output: process.stdout });
19
- const io = {
20
- ask: (question: string): Promise<string> =>
21
- new Promise((resolve) => rl.question(question, (answer) => resolve(answer.trim()))),
22
- write: (msg: string) => process.stdout.write(msg),
23
- };
24
- settings = await runSetupWizard(io);
25
- rl.close();
26
- saveSettings(settings, defaultDir);
27
- log.info("Settings saved");
28
- }
14
+ if (!settings) {
15
+ log.error("No settings found. Run `macroclaw setup` first.");
16
+ process.exit(1);
17
+ }
29
18
 
30
- const { settings: resolved, overrides } = applyEnvOverrides(settings);
31
- settings = resolved;
19
+ const { settings: resolved, overrides } = applyEnvOverrides(settings);
32
20
 
33
- printSettings(settings, overrides);
21
+ await initLogger({ level: resolved.logLevel, pinoramaUrl: resolved.pinoramaUrl });
22
+ printSettings(resolved, overrides);
34
23
 
35
- const workspace = resolve(settings.workspace.replace(/^~/, process.env.HOME || "~"));
24
+ const workspace = resolve(resolved.workspace.replace(/^~/, process.env.HOME || "~"));
36
25
 
37
- function initWorkspace(workspace: string) {
38
- const templateDir = join(dirname(import.meta.dir), "workspace-template");
39
- const exists = existsSync(workspace);
40
- const empty = exists && readdirSync(workspace).length === 0;
26
+ function initWorkspace(workspace: string) {
27
+ const templateDir = join(dirname(import.meta.dir), "workspace-template");
28
+ const exists = existsSync(workspace);
29
+ const empty = exists && readdirSync(workspace).length === 0;
41
30
 
42
- if (!exists || empty) {
43
- log.info({ workspace }, "Initializing workspace from template");
44
- cpSync(templateDir, workspace, { recursive: true });
45
- log.info("Workspace initialized");
31
+ if (!exists || empty) {
32
+ log.info({ workspace }, "Initializing workspace from template");
33
+ cpSync(templateDir, workspace, { recursive: true });
34
+ log.info("Workspace initialized");
35
+ }
46
36
  }
47
- }
48
37
 
49
- initWorkspace(workspace);
38
+ initWorkspace(workspace);
50
39
 
51
- const config: AppConfig = {
52
- botToken: settings.botToken,
53
- authorizedChatId: settings.chatId,
54
- workspace,
55
- model: settings.model,
56
- };
40
+ const config: AppConfig = {
41
+ botToken: resolved.botToken,
42
+ authorizedChatId: resolved.chatId,
43
+ workspace,
44
+ model: resolved.model,
45
+ };
57
46
 
58
- new App(config).start();
47
+ new App(config).start();
48
+ }
@@ -17,17 +17,28 @@ describe("createLogger", () => {
17
17
  });
18
18
 
19
19
  describe("initLogger", () => {
20
- it("adds pinorama transport when PINORAMA_URL is set", async () => {
21
- process.env.PINORAMA_URL = "http://localhost:6200/pinorama";
20
+ it("does nothing when called without opts", async () => {
21
+ mockPinoramaTransport.mockClear();
22
22
  await initLogger();
23
- expect(mockPinoramaTransport).toHaveBeenCalledWith({ url: "http://localhost:6200/pinorama" });
24
- delete process.env.PINORAMA_URL;
23
+ expect(mockPinoramaTransport).not.toHaveBeenCalled();
24
+ });
25
+
26
+ it("sets log level from opts", async () => {
27
+ const log = createLogger("opts-level");
28
+ await initLogger({ level: "warn" });
29
+ expect(log.level).toBe("warn");
30
+ await initLogger({ level: "info" }); // restore
25
31
  });
26
32
 
27
- it("does nothing when PINORAMA_URL is not set", async () => {
28
- delete process.env.PINORAMA_URL;
33
+ it("adds pinorama transport from opts", async () => {
29
34
  mockPinoramaTransport.mockClear();
30
- await initLogger();
35
+ await initLogger({ pinoramaUrl: "http://example.com/pinorama" });
36
+ expect(mockPinoramaTransport).toHaveBeenCalledWith({ url: "http://example.com/pinorama" });
37
+ });
38
+
39
+ it("does not add duplicate pinorama transport on second call", async () => {
40
+ mockPinoramaTransport.mockClear();
41
+ await initLogger({ pinoramaUrl: "http://example.com/pinorama" });
31
42
  expect(mockPinoramaTransport).not.toHaveBeenCalled();
32
43
  });
33
44
  });
package/src/logger.ts CHANGED
@@ -6,20 +6,30 @@ const prettyStream = pretty({
6
6
  messageFormat: "[{module}] {msg}",
7
7
  });
8
8
 
9
- const level = (process.env.LOG_LEVEL || "info") as pino.Level;
9
+ const defaultLevel = (process.env.LOG_LEVEL || "info") as pino.Level;
10
10
 
11
- const streams: pino.StreamEntry[] = [{ level, stream: prettyStream }];
11
+ // Streams accept all levels; logger.level is the sole gate
12
+ const streams: pino.StreamEntry[] = [{ level: "trace", stream: prettyStream }];
12
13
 
13
14
  const logger = pino(
14
- { level: process.env.LOG_LEVEL || "info" },
15
+ { level: defaultLevel },
15
16
  pino.multistream(streams),
16
17
  );
17
18
 
18
- export async function initLogger(): Promise<void> {
19
- const pinoramaUrl = process.env.PINORAMA_URL;
20
- if (pinoramaUrl) {
19
+ export interface LoggerOptions {
20
+ level?: pino.Level;
21
+ pinoramaUrl?: string;
22
+ }
23
+
24
+ let pinoramaAdded = false;
25
+
26
+ export async function initLogger(opts?: LoggerOptions): Promise<void> {
27
+ if (opts?.level) logger.level = opts.level;
28
+
29
+ if (opts?.pinoramaUrl && !pinoramaAdded) {
21
30
  const { default: pinoramaTransport } = await import("pinorama-transport");
22
- streams.push({ level, stream: pinoramaTransport({ url: pinoramaUrl }) });
31
+ streams.push({ level: "trace", stream: pinoramaTransport({ url: opts.pinoramaUrl }) });
32
+ pinoramaAdded = true;
23
33
  }
24
34
  }
25
35