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 +56 -46
- package/dist/config.d.ts +1 -0
- package/dist/config.js +33 -10
- package/dist/notifier.js +19 -17
- package/package.json +1 -1
- package/src/config.ts +39 -12
- package/src/notifier.ts +25 -22
package/README.md
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
|
-
# nnt
|
|
1
|
+
# nnt
|
|
2
2
|
|
|
3
|
-
|
|
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
|
|
11
|
+
After installation, `nnt` is available globally.
|
|
14
12
|
|
|
15
|
-
##
|
|
13
|
+
## Add profile
|
|
16
14
|
|
|
17
|
-
|
|
18
|
-
|
|
15
|
+
```bash
|
|
16
|
+
nnt profile add
|
|
17
|
+
```
|
|
19
18
|
|
|
20
|
-
|
|
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
|
-
|
|
33
|
+
nnt "Cool message using nnt"
|
|
24
34
|
```
|
|
25
35
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
## Add profile
|
|
36
|
+
Send with a specific profile:
|
|
29
37
|
|
|
30
38
|
```bash
|
|
31
|
-
nnt profile
|
|
39
|
+
nnt "some urgent message" --profile=important
|
|
32
40
|
```
|
|
33
41
|
|
|
34
|
-
|
|
42
|
+
Typical agent usage:
|
|
35
43
|
|
|
36
44
|
```bash
|
|
37
|
-
|
|
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
|
-
|
|
51
|
+
## Install skills
|
|
41
52
|
|
|
42
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
##
|
|
95
|
-
|
|
96
|
-
Send using default profile:
|
|
105
|
+
## Config location
|
|
97
106
|
|
|
98
|
-
|
|
99
|
-
nnt "Default message"
|
|
100
|
-
```
|
|
107
|
+
Config is stored as JSON at `<config-dir>/nnt.json`.
|
|
101
108
|
|
|
102
|
-
|
|
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
|
-
|
|
105
|
-
nnt "some message for user" --profile=important-profile
|
|
106
|
-
```
|
|
107
|
-
|
|
108
|
-
Equivalent explicit command:
|
|
113
|
+
Example:
|
|
109
114
|
|
|
110
115
|
```bash
|
|
111
|
-
|
|
116
|
+
export NNT_CONFIG_DIR="$HOME/.custom-config/custom-nnt"
|
|
112
117
|
```
|
|
113
118
|
|
|
114
|
-
|
|
119
|
+
If you run coding agents in a container, mount the config directory as read-only:
|
|
115
120
|
|
|
116
|
-
```
|
|
117
|
-
|
|
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
|
-
##
|
|
130
|
+
## API
|
|
121
131
|
|
|
122
|
-
`
|
|
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
|
-
|
|
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
|
|
16
|
+
return DEFAULT_CONFIG_DIR;
|
|
14
17
|
}
|
|
15
18
|
export function getConfigPath() {
|
|
16
|
-
return join(getConfigDir(),
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
40
|
-
|
|
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
|
|
43
|
+
return {
|
|
44
|
+
profiles: [],
|
|
45
|
+
defaultProfile: null,
|
|
46
|
+
};
|
|
45
47
|
}
|
|
46
48
|
}
|
|
47
49
|
export class Notifier {
|
package/package.json
CHANGED
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
|
|
33
|
+
return DEFAULT_CONFIG_DIR;
|
|
30
34
|
}
|
|
31
35
|
|
|
32
36
|
export function getConfigPath(): string {
|
|
33
|
-
return join(getConfigDir(),
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
78
|
-
|
|
79
|
-
|
|
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
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
|
101
|
+
return {
|
|
102
|
+
profiles: [],
|
|
103
|
+
defaultProfile: null,
|
|
104
|
+
};
|
|
102
105
|
}
|
|
103
106
|
}
|
|
104
107
|
|