nonotify 0.1.5 → 0.1.7

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/README.md CHANGED
@@ -1,8 +1,6 @@
1
- # nnt (nonotify)
1
+ # nnt
2
2
 
3
- Terminal-first notifier for Telegram.
4
-
5
- Use it to send yourself a message when tasks are done, including from coding agents.
3
+ А terminal-first notifier built with [incur](https://github.com/wevm/incur). Use it to send yourself messages from the terminal, including from coding agents and CI jobs.
6
4
 
7
5
  ## Install
8
6
 
@@ -10,41 +8,54 @@ Use it to send yourself a message when tasks are done, including from coding age
10
8
  npm install -g nonotify
11
9
  ```
12
10
 
13
- After install, `nnt` is available globally.
11
+ After installation, `nnt` is available globally.
14
12
 
15
- ## Config location
13
+ ## Add profile
16
14
 
17
- - Default: `~/.nnt/config`
18
- - Override: set `NNT_CONFIG_DIR`
15
+ ```bash
16
+ nnt profile add
17
+ ```
19
18
 
20
- Example:
19
+ Flow:
20
+
21
+ 1. Enter profile name.
22
+ 2. Enter Telegram bot token.
23
+ 3. Send any message to your bot in Telegram.
24
+ 4. CLI captures `chatId`, shows connected Telegram `username`, stores the profile, and sends a confirmation message back to chat.
25
+
26
+ The first profile becomes default profile automatically.
27
+
28
+ ## Send messages
29
+
30
+ Send with the default profile:
21
31
 
22
32
  ```bash
23
- export NNT_CONFIG_DIR="$HOME/.config/nnt"
33
+ nnt "Cool message using nnt"
24
34
  ```
25
35
 
26
- Config is stored as JSON in `<config-dir>/config`.
27
-
28
- ## Add profile
36
+ Send with a specific profile:
29
37
 
30
38
  ```bash
31
- nnt profile add
39
+ nnt "some urgent message" --profile=important
32
40
  ```
33
41
 
34
- Optional explicit provider form:
42
+ Typical agent usage:
35
43
 
36
44
  ```bash
37
- nnt profile add telegram
45
+ # User: Complete a long task, while I'm away, when finish notify me via nnt
46
+ # Agent: *working*
47
+ # Agent after task completed:
48
+ nnt "Very long task finished. All tests passed, check out result"
38
49
  ```
39
50
 
40
- Flow:
51
+ ## Install skills
41
52
 
42
- 1. Enter profile name.
43
- 2. Enter Telegram bot token.
44
- 3. Send any message to your bot in Telegram.
45
- 4. CLI captures `chat_id`, shows connected Telegram `username`, stores the profile, and sends a confirmation message back to chat.
53
+ You can install agent skills for your agents:
46
54
 
47
- The first profile becomes default profile automatically.
55
+ ```bash
56
+ nnt skills add # install skills globally
57
+ cp ~/.agents/skills/nnt-* ./.agents/skills/ # install in current project
58
+ ```
48
59
 
49
60
  ## Manage profiles
50
61
 
@@ -54,19 +65,19 @@ List profiles:
54
65
  nnt profile list
55
66
  ```
56
67
 
57
- Show default profile:
68
+ Show the default profile:
58
69
 
59
70
  ```bash
60
71
  nnt profile default
61
72
  ```
62
73
 
63
- Set default profile:
74
+ Set the default profile:
64
75
 
65
76
  ```bash
66
77
  nnt profile default important-profile
67
78
  ```
68
79
 
69
- Edit profile (rename, token/chat update, reconnect):
80
+ Edit a profile (rename, token/chat update, reconnect):
70
81
 
71
82
  ```bash
72
83
  nnt profile edit
@@ -76,7 +87,7 @@ nnt profile edit critical-profile --botToken=123:abc
76
87
  nnt profile edit critical-profile --reconnect
77
88
  ```
78
89
 
79
- `nnt profile edit` starts interactive mode and asks you to select a profile first.
90
+ `nnt profile edit` starts interactive mode and prompts you to select a profile first.
80
91
 
81
92
  Delete profile:
82
93
 
@@ -84,42 +95,41 @@ Delete profile:
84
95
  nnt profile delete critical-profile
85
96
  ```
86
97
 
87
- By default, profile commands print human-readable output in terminal. For strict machine-friendly output, use format flags:
98
+ By default, profile commands print human-readable output in terminal. For strict, machine-friendly output, use format flags:
88
99
 
89
100
  ```bash
90
101
  nnt profile list --format json
91
102
  nnt profile default --format=md
92
103
  ```
93
104
 
94
- ## Send messages
95
-
96
- Send using default profile:
105
+ ## Config location
97
106
 
98
- ```bash
99
- nnt "Default message"
100
- ```
107
+ Config is stored as JSON at `<config-dir>/nnt.json`.
101
108
 
102
- Send using specific profile:
109
+ - Default config dir: `~/.config/nnt`
110
+ - Default config path: `~/.config/nnt/nnt.json`
111
+ - To override it, set `NNT_CONFIG_DIR`
103
112
 
104
- ```bash
105
- nnt "some message for user" --profile=important-profile
106
- ```
107
-
108
- Equivalent explicit command:
113
+ Example:
109
114
 
110
115
  ```bash
111
- nnt send "some message for user" --profile=important-profile
116
+ export NNT_CONFIG_DIR="$HOME/.custom-config/custom-nnt"
112
117
  ```
113
118
 
114
- ## Typical agent usage
119
+ If you run coding agents in a container, mount the config directory as read-only:
115
120
 
116
- ```bash
117
- nnt "Task finished: migrations applied and tests passed"
121
+ ```yaml
122
+ services:
123
+ app:
124
+ environment:
125
+ - NNT_CONFIG_DIR=/var/nnt
126
+ volumes:
127
+ - ${HOME}/.config/nnt:/var/nnt:ro
118
128
  ```
119
129
 
120
- ## Node.js API
130
+ ## API
121
131
 
122
- `Notifier` loads config using `EnvConfigLoader` by default.
132
+ You can integrate `nnt` into your application. Useful when buildling extensions for coding agents. The `Notifier` automaticly loads profile information, so you can send messages easily.
123
133
 
124
134
  ```ts
125
135
  import { Notifier } from "nonotify";
@@ -131,7 +141,7 @@ await notifier.send({
131
141
  });
132
142
  ```
133
143
 
134
- Also you can pass profile data directly.
144
+ Notifier loads config using `EnvConfigLoader` by default, but you can also pass profile data directly.
135
145
 
136
146
  ```ts
137
147
  import { Notifier } from "nonotify";
package/dist/config.d.ts CHANGED
@@ -11,5 +11,6 @@ export type NntConfig = {
11
11
  };
12
12
  export declare function getConfigDir(): string;
13
13
  export declare function getConfigPath(): string;
14
+ export declare function getLegacyConfigPath(): string;
14
15
  export declare function loadConfig(): Promise<NntConfig>;
15
16
  export declare function saveConfig(config: NntConfig): Promise<void>;
package/dist/config.js CHANGED
@@ -5,31 +5,47 @@ const DEFAULT_CONFIG = {
5
5
  defaultProfile: null,
6
6
  profiles: {},
7
7
  };
8
+ const CONFIG_FILENAME = "nnt.json";
9
+ const DEFAULT_CONFIG_DIR = join(homedir(), ".config", "nnt");
10
+ const LEGACY_CONFIG_PATH = join(homedir(), ".nnt", "config");
8
11
  export function getConfigDir() {
9
12
  const dir = process.env.NNT_CONFIG_DIR;
10
13
  if (dir && dir.trim() !== "") {
11
14
  return resolve(dir);
12
15
  }
13
- return join(homedir(), ".nnt");
16
+ return DEFAULT_CONFIG_DIR;
14
17
  }
15
18
  export function getConfigPath() {
16
- return join(getConfigDir(), "config");
19
+ return join(getConfigDir(), CONFIG_FILENAME);
20
+ }
21
+ export function getLegacyConfigPath() {
22
+ return LEGACY_CONFIG_PATH;
17
23
  }
18
24
  export async function loadConfig() {
19
25
  const path = getConfigPath();
20
26
  try {
21
27
  const raw = await readFile(path, "utf8");
22
- const parsed = JSON.parse(raw);
23
- return {
24
- defaultProfile: typeof parsed.defaultProfile === "string"
25
- ? parsed.defaultProfile
26
- : null,
27
- profiles: parsed.profiles ?? {},
28
- };
28
+ return parseConfig(raw);
29
29
  }
30
30
  catch (error) {
31
31
  if (isNodeError(error) && error.code === "ENOENT") {
32
- return { ...DEFAULT_CONFIG };
32
+ try {
33
+ const legacyRaw = await readFile(getLegacyConfigPath(), "utf8");
34
+ const parsed = parseConfig(legacyRaw);
35
+ try {
36
+ await saveConfig(parsed);
37
+ }
38
+ catch {
39
+ // Best effort migration only.
40
+ }
41
+ return parsed;
42
+ }
43
+ catch (legacyError) {
44
+ if (isNodeError(legacyError) && legacyError.code === "ENOENT") {
45
+ return { ...DEFAULT_CONFIG };
46
+ }
47
+ throw legacyError;
48
+ }
33
49
  }
34
50
  throw error;
35
51
  }
@@ -51,3 +67,10 @@ export async function saveConfig(config) {
51
67
  function isNodeError(error) {
52
68
  return typeof error === "object" && error !== null && "code" in error;
53
69
  }
70
+ function parseConfig(raw) {
71
+ const parsed = JSON.parse(raw);
72
+ return {
73
+ defaultProfile: typeof parsed.defaultProfile === "string" ? parsed.defaultProfile : null,
74
+ profiles: parsed.profiles ?? {},
75
+ };
76
+ }
package/dist/notifier.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { readFileSync } from "node:fs";
2
- import { getConfigPath } from "./config.js";
2
+ import { getConfigPath, getLegacyConfigPath } from "./config.js";
3
3
  import { sendTelegramMessage } from "./telegram.js";
4
4
  export class NotifierError extends Error {
5
5
  constructor(message) {
@@ -23,25 +23,27 @@ export class NoProfilesConfiguredError extends NotifierError {
23
23
  }
24
24
  export class EnvConfigLoader {
25
25
  load() {
26
- const configPath = getConfigPath();
27
- let rawConfig;
28
- try {
29
- const raw = readFileSync(configPath, "utf8");
30
- rawConfig = JSON.parse(raw);
31
- }
32
- catch (error) {
33
- if (isNodeError(error) && error.code === "ENOENT") {
34
- return {
35
- profiles: [],
36
- defaultProfile: null,
37
- };
26
+ const configPaths = Array.from(new Set([getConfigPath(), getLegacyConfigPath()]));
27
+ for (const configPath of configPaths) {
28
+ try {
29
+ const raw = readFileSync(configPath, "utf8");
30
+ const rawConfig = JSON.parse(raw);
31
+ return normalizeEnvConfig(rawConfig);
38
32
  }
39
- if (error instanceof SyntaxError) {
40
- throw new NotifierError(`Invalid JSON in config file at ${configPath}: ${error.message}`);
33
+ catch (error) {
34
+ if (isNodeError(error) && error.code === "ENOENT") {
35
+ continue;
36
+ }
37
+ if (error instanceof SyntaxError) {
38
+ throw new NotifierError(`Invalid JSON in config file at ${configPath}: ${error.message}`);
39
+ }
40
+ throw error;
41
41
  }
42
- throw error;
43
42
  }
44
- return normalizeEnvConfig(rawConfig);
43
+ return {
44
+ profiles: [],
45
+ defaultProfile: null,
46
+ };
45
47
  }
46
48
  }
47
49
  export class Notifier {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nonotify",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
4
4
  "description": "nnt CLI for Telegram notifications",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
package/src/config.ts CHANGED
@@ -20,17 +20,25 @@ const DEFAULT_CONFIG: NntConfig = {
20
20
  profiles: {},
21
21
  };
22
22
 
23
+ const CONFIG_FILENAME = "nnt.json";
24
+ const DEFAULT_CONFIG_DIR = join(homedir(), ".config", "nnt");
25
+ const LEGACY_CONFIG_PATH = join(homedir(), ".nnt", "config");
26
+
23
27
  export function getConfigDir(): string {
24
28
  const dir = process.env.NNT_CONFIG_DIR;
25
29
  if (dir && dir.trim() !== "") {
26
30
  return resolve(dir);
27
31
  }
28
32
 
29
- return join(homedir(), ".nnt");
33
+ return DEFAULT_CONFIG_DIR;
30
34
  }
31
35
 
32
36
  export function getConfigPath(): string {
33
- return join(getConfigDir(), "config");
37
+ return join(getConfigDir(), CONFIG_FILENAME);
38
+ }
39
+
40
+ export function getLegacyConfigPath(): string {
41
+ return LEGACY_CONFIG_PATH;
34
42
  }
35
43
 
36
44
  export async function loadConfig(): Promise<NntConfig> {
@@ -38,18 +46,27 @@ export async function loadConfig(): Promise<NntConfig> {
38
46
 
39
47
  try {
40
48
  const raw = await readFile(path, "utf8");
41
- const parsed = JSON.parse(raw) as Partial<NntConfig>;
42
-
43
- return {
44
- defaultProfile:
45
- typeof parsed.defaultProfile === "string"
46
- ? parsed.defaultProfile
47
- : null,
48
- profiles: parsed.profiles ?? {},
49
- };
49
+ return parseConfig(raw);
50
50
  } catch (error) {
51
51
  if (isNodeError(error) && error.code === "ENOENT") {
52
- return { ...DEFAULT_CONFIG };
52
+ try {
53
+ const legacyRaw = await readFile(getLegacyConfigPath(), "utf8");
54
+ const parsed = parseConfig(legacyRaw);
55
+
56
+ try {
57
+ await saveConfig(parsed);
58
+ } catch {
59
+ // Best effort migration only.
60
+ }
61
+
62
+ return parsed;
63
+ } catch (legacyError) {
64
+ if (isNodeError(legacyError) && legacyError.code === "ENOENT") {
65
+ return { ...DEFAULT_CONFIG };
66
+ }
67
+
68
+ throw legacyError;
69
+ }
53
70
  }
54
71
 
55
72
  throw error;
@@ -75,3 +92,13 @@ export async function saveConfig(config: NntConfig): Promise<void> {
75
92
  function isNodeError(error: unknown): error is NodeJS.ErrnoException {
76
93
  return typeof error === "object" && error !== null && "code" in error;
77
94
  }
95
+
96
+ function parseConfig(raw: string): NntConfig {
97
+ const parsed = JSON.parse(raw) as Partial<NntConfig>;
98
+
99
+ return {
100
+ defaultProfile:
101
+ typeof parsed.defaultProfile === "string" ? parsed.defaultProfile : null,
102
+ profiles: parsed.profiles ?? {},
103
+ };
104
+ }
package/src/notifier.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { readFileSync } from "node:fs";
2
- import { getConfigPath } from "./config.js";
2
+ import { getConfigPath, getLegacyConfigPath } from "./config.js";
3
3
  import { sendTelegramMessage } from "./telegram.js";
4
4
 
5
5
  export type NotifierProfile = {
@@ -74,31 +74,34 @@ type RawRecordProfile = {
74
74
 
75
75
  export class EnvConfigLoader implements NotifierConfigLoader {
76
76
  load(): NotifierConfig {
77
- const configPath = getConfigPath();
78
-
79
- let rawConfig: RawEnvConfig;
80
-
81
- try {
82
- const raw = readFileSync(configPath, "utf8");
83
- rawConfig = JSON.parse(raw) as RawEnvConfig;
84
- } catch (error) {
85
- if (isNodeError(error) && error.code === "ENOENT") {
86
- return {
87
- profiles: [],
88
- defaultProfile: null,
89
- };
90
- }
77
+ const configPaths = Array.from(
78
+ new Set([getConfigPath(), getLegacyConfigPath()]),
79
+ );
91
80
 
92
- if (error instanceof SyntaxError) {
93
- throw new NotifierError(
94
- `Invalid JSON in config file at ${configPath}: ${error.message}`,
95
- );
81
+ for (const configPath of configPaths) {
82
+ try {
83
+ const raw = readFileSync(configPath, "utf8");
84
+ const rawConfig = JSON.parse(raw) as RawEnvConfig;
85
+ return normalizeEnvConfig(rawConfig);
86
+ } catch (error) {
87
+ if (isNodeError(error) && error.code === "ENOENT") {
88
+ continue;
89
+ }
90
+
91
+ if (error instanceof SyntaxError) {
92
+ throw new NotifierError(
93
+ `Invalid JSON in config file at ${configPath}: ${error.message}`,
94
+ );
95
+ }
96
+
97
+ throw error;
96
98
  }
97
-
98
- throw error;
99
99
  }
100
100
 
101
- return normalizeEnvConfig(rawConfig);
101
+ return {
102
+ profiles: [],
103
+ defaultProfile: null,
104
+ };
102
105
  }
103
106
  }
104
107