assurgent 0.1.0 → 0.2.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/cli.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env bun
2
2
  // Entry point for `bunx assurgent`
3
3
 
4
- import { readFileSync } from "node:fs";
5
- import { join } from "node:path";
4
+ import { copyFileSync, existsSync, mkdirSync, readFileSync } from "node:fs";
5
+ import { dirname, join } from "node:path";
6
6
 
7
7
  const args = process.argv.slice(2);
8
8
 
@@ -11,15 +11,16 @@ if (args.includes("--help") || args.includes("-h")) {
11
11
  assurgent - Telegram bot bridge to Claude Code CLI
12
12
 
13
13
  Usage:
14
- assurgent [options]
15
-
16
- Options:
17
- --help, -h Show this help message
18
- --version, -v Print the version number
14
+ assurgent Start the bot
15
+ assurgent init Scaffold config.json into ASSURGENT_HOME
16
+ assurgent --help Show this help message
17
+ assurgent --version Print the version number
19
18
 
20
19
  Configuration:
21
- Requires config.json and .env in the working directory.
22
- See config.example.json for the expected structure.
20
+ Config is loaded from $ASSURGENT_HOME/config.json.
21
+ ASSURGENT_HOME defaults to ~/.assurgent/ if not set.
22
+
23
+ Run "assurgent init" to create the config file, then edit it with your settings.
23
24
  `);
24
25
  process.exit(0);
25
26
  }
@@ -31,4 +32,22 @@ if (args.includes("--version") || args.includes("-v")) {
31
32
  process.exit(0);
32
33
  }
33
34
 
35
+ if (args[0] === "init") {
36
+ const { getAssurgentHome } = await import("./src/config.ts");
37
+ const target = join(getAssurgentHome(), "config.json");
38
+
39
+ if (existsSync(target)) {
40
+ console.error(
41
+ `Config already exists at ${target}. Edit it directly or delete it to re-initialize.`,
42
+ );
43
+ process.exit(1);
44
+ }
45
+
46
+ mkdirSync(dirname(target), { recursive: true });
47
+ const source = join(import.meta.dirname, "config.example.json");
48
+ copyFileSync(source, target);
49
+ console.log(`Created config at ${target}. Edit it with your settings, then run "assurgent".`);
50
+ process.exit(0);
51
+ }
52
+
34
53
  await import("./src/index.ts");
@@ -0,0 +1,26 @@
1
+ {
2
+ "chat": {
3
+ "adapter": "telegram",
4
+ "telegram": {
5
+ "botToken": "123456:ABC-DEF...",
6
+ "allowedUserIds": ["YOUR_TELEGRAM_USER_ID"],
7
+ "placeholder": {
8
+ "enabled": true,
9
+ "text": "thinking..."
10
+ }
11
+ }
12
+ },
13
+ "agent": {
14
+ "adapter": "claude-code",
15
+ "claude-code": {
16
+ "model": "sonnet",
17
+ "maxTurns": 10,
18
+ "flags": ["--dangerously-skip-permissions"],
19
+ "claudePath": "claude"
20
+ }
21
+ },
22
+ "session": {
23
+ "turnLimit": 20
24
+ },
25
+ "workspacePath": "/path/to/your/workspace"
26
+ }
package/package.json CHANGED
@@ -1,15 +1,11 @@
1
1
  {
2
2
  "name": "assurgent",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "assurgent": "./cli.ts"
7
7
  },
8
- "files": [
9
- "cli.ts",
10
- "src",
11
- "README.md"
12
- ],
8
+ "files": ["cli.ts", "src", "config.example.json", "README.md"],
13
9
  "publishConfig": {
14
10
  "access": "public"
15
11
  },
@@ -1,5 +1,8 @@
1
- import { describe, expect, test } from "bun:test";
2
- import { validateConfig } from "./config";
1
+ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
2
+ import fs from "node:fs";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ import { getAssurgentHome, loadConfig, validateConfig } from "./config";
3
6
  import type { Config } from "./config";
4
7
 
5
8
  function validConfig(): Config {
@@ -26,6 +29,82 @@ function validConfig(): Config {
26
29
  };
27
30
  }
28
31
 
32
+ describe("getAssurgentHome", () => {
33
+ const originalEnv = process.env.ASSURGENT_HOME;
34
+
35
+ afterEach(() => {
36
+ if (originalEnv === undefined) {
37
+ process.env.ASSURGENT_HOME = undefined;
38
+ } else {
39
+ process.env.ASSURGENT_HOME = originalEnv;
40
+ }
41
+ });
42
+
43
+ test("returns ~/.assurgent when ASSURGENT_HOME is not set", () => {
44
+ process.env.ASSURGENT_HOME = undefined;
45
+ const expected = path.join(os.homedir(), ".assurgent");
46
+ expect(getAssurgentHome()).toBe(expected);
47
+ });
48
+
49
+ test("returns ASSURGENT_HOME env var value when set", () => {
50
+ process.env.ASSURGENT_HOME = "/custom/assurgent/home";
51
+ expect(getAssurgentHome()).toBe("/custom/assurgent/home");
52
+ });
53
+ });
54
+
55
+ describe("loadConfig", () => {
56
+ let tempDir: string;
57
+
58
+ beforeEach(() => {
59
+ tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "assurgent-test-"));
60
+ });
61
+
62
+ afterEach(() => {
63
+ fs.rmSync(tempDir, { recursive: true, force: true });
64
+ process.env.ASSURGENT_HOME = undefined;
65
+ });
66
+
67
+ function writeConfig(dir: string, config: Config): string {
68
+ const configPath = path.join(dir, "config.json");
69
+ fs.writeFileSync(configPath, JSON.stringify(config), "utf-8");
70
+ return configPath;
71
+ }
72
+
73
+ test("reads config from explicit path", () => {
74
+ const configPath = writeConfig(tempDir, validConfig());
75
+ const config = loadConfig(configPath);
76
+ expect(config.chat.adapter).toBe("telegram");
77
+ expect(config.session.turnLimit).toBe(20);
78
+ });
79
+
80
+ test("reads config from $ASSURGENT_HOME/config.json when no explicit path", () => {
81
+ process.env.ASSURGENT_HOME = tempDir;
82
+ writeConfig(tempDir, validConfig());
83
+ const config = loadConfig();
84
+ expect(config.chat.adapter).toBe("telegram");
85
+ });
86
+
87
+ test("throws with helpful error when config is missing and explicit path given", () => {
88
+ const missingPath = path.join(tempDir, "nonexistent.json");
89
+ expect(() => loadConfig(missingPath)).toThrow("Config file not found");
90
+ });
91
+
92
+ test("throws mentioning 'assurgent init' when config is missing", () => {
93
+ const missingPath = path.join(tempDir, "nonexistent.json");
94
+ expect(() => loadConfig(missingPath)).toThrow("assurgent init");
95
+ });
96
+
97
+ test("throws mentioning 'ASSURGENT_HOME' when config is missing", () => {
98
+ const missingPath = path.join(tempDir, "nonexistent.json");
99
+ expect(() => loadConfig(missingPath)).toThrow("ASSURGENT_HOME");
100
+ });
101
+
102
+ test("throws with config path in error message when config is missing", () => {
103
+ process.env.ASSURGENT_HOME = tempDir;
104
+ expect(() => loadConfig()).toThrow(path.join(tempDir, "config.json"));
105
+ });
106
+ });
107
+
29
108
  describe("validateConfig", () => {
30
109
  test("accepts valid config", () => {
31
110
  expect(() => validateConfig(validConfig())).not.toThrow();
package/src/config.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import fs from "node:fs";
2
+ import os from "node:os";
2
3
  import path from "node:path";
3
4
 
4
5
  /** Runtime configuration for the bot. */
@@ -31,6 +32,11 @@ export interface Config {
31
32
  workspacePath: string;
32
33
  }
33
34
 
35
+ /** Returns the resolved ASSURGENT_HOME path. */
36
+ export function getAssurgentHome(): string {
37
+ return process.env.ASSURGENT_HOME ?? path.join(os.homedir(), ".assurgent");
38
+ }
39
+
34
40
  /** Fail fast with clear errors if required config fields are missing or invalid. */
35
41
  export function validateConfig(config: Config): void {
36
42
  const errors: string[] = [];
@@ -71,10 +77,12 @@ export function validateConfig(config: Config): void {
71
77
 
72
78
  /** Load and validate config from a JSON file. */
73
79
  export function loadConfig(configPath?: string): Config {
74
- const resolved = configPath ?? path.resolve(import.meta.dir, "..", "config.json");
80
+ const resolved = configPath ?? path.join(getAssurgentHome(), "config.json");
75
81
 
76
82
  if (!fs.existsSync(resolved)) {
77
- throw new Error(`Config file not found: ${resolved}`);
83
+ throw new Error(
84
+ `Config file not found: ${resolved}\nRun "assurgent init" to create one, or set ASSURGENT_HOME to point to an existing config directory.`,
85
+ );
78
86
  }
79
87
 
80
88
  const raw = JSON.parse(fs.readFileSync(resolved, "utf-8"));
package/src/index.ts CHANGED
@@ -2,7 +2,7 @@ import path from "node:path";
2
2
  import { ClaudeCodeAdapter } from "./agent/claude-code";
3
3
  import { TelegramAdapter } from "./chat/telegram";
4
4
  import type { Config } from "./config";
5
- import { loadConfig } from "./config";
5
+ import { getAssurgentHome, loadConfig } from "./config";
6
6
  import { SessionManager } from "./core/session-manager";
7
7
  import { Wrapper } from "./core/wrapper";
8
8
  import type { AgentAdapter } from "./interfaces/agent-adapter";
@@ -33,7 +33,7 @@ function createAgentAdapter(cfg: Config): AgentAdapter {
33
33
  const chat = createChatAdapter(config);
34
34
  const agent = createAgentAdapter(config);
35
35
  const sessions = new SessionManager({
36
- statePath: path.join(config.workspacePath, "state"),
36
+ statePath: path.join(getAssurgentHome(), "state"),
37
37
  });
38
38
 
39
39
  const wrapper = new Wrapper(