claude-notification-plugin 1.1.76 → 1.1.81
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/.claude-plugin/plugin.json +1 -1
- package/README.md +34 -0
- package/bin/constants.js +76 -0
- package/commit-sha +1 -1
- package/listener/LISTENER-DETAILED.md +51 -0
- package/listener/listener.js +216 -1
- package/notifier/notifier.js +13 -7
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-notification-plugin",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.81",
|
|
4
4
|
"description": "Claude Code task-completion notifications: Telegram, desktop notifications (Windows/macOS/Linux), sound, and voice",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Viacheslav Makarov",
|
package/README.md
CHANGED
|
@@ -250,6 +250,11 @@ Projects are referenced with the `&` prefix (e.g. `&api`, `&api/branch`).
|
|
|
250
250
|
| `/clear &project[/branch]` | Clear queue + reset session |
|
|
251
251
|
| `/newsession [&project[/branch]]` | Reset session only (keep queue) |
|
|
252
252
|
| `/projects` | List projects and paths |
|
|
253
|
+
| `/addproject <alias> <path>` | Register a project alias |
|
|
254
|
+
| `/addproject <alias> /<basename>` | Register using basename from `/seen` |
|
|
255
|
+
| `/seen` | Recent folders seen by notifier |
|
|
256
|
+
| `/seen clear` | Wipe the seen list |
|
|
257
|
+
| `/setdefault` | Change the default project |
|
|
253
258
|
| `/worktrees &project` | List worktrees |
|
|
254
259
|
| `/worktree &project/branch` | Create a worktree |
|
|
255
260
|
| `/rmworktree &project/branch` | Remove a worktree |
|
|
@@ -260,6 +265,35 @@ Projects are referenced with the `&` prefix (e.g. `&api`, `&api/branch`).
|
|
|
260
265
|
| `/menu` | Show help with inline buttons |
|
|
261
266
|
| `/help` | Show help with inline buttons |
|
|
262
267
|
|
|
268
|
+
#### Registering projects on the fly (`/addproject` + `/seen`)
|
|
269
|
+
|
|
270
|
+
Whenever the notifier fires for a folder, it records the absolute path,
|
|
271
|
+
basename and timestamp in `~/.claude/claude-notify.seen.json` (last 30
|
|
272
|
+
entries, oldest auto-evicted). `/seen` shows that list as a table — folders
|
|
273
|
+
already registered as listener projects have their alias in the `alias`
|
|
274
|
+
column; the rest show `—`.
|
|
275
|
+
|
|
276
|
+
`/addproject` accepts two forms for the path argument:
|
|
277
|
+
|
|
278
|
+
```
|
|
279
|
+
/addproject mj D:/DEV/FA/_cur/mcp-jira — explicit absolute path
|
|
280
|
+
/addproject mj /mcp-jira — basename from /seen (most recent)
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
The basename form (`/name`) looks up the most recent entry in the seen file
|
|
284
|
+
whose basename equals `name`. This is handy right after receiving a
|
|
285
|
+
notification like `✅ /mcp-jira/master` — no need to retype the full path.
|
|
286
|
+
|
|
287
|
+
Safeguards: alias must be unique, path must exist and be a directory, and
|
|
288
|
+
the same path cannot be registered twice under different aliases.
|
|
289
|
+
|
|
290
|
+
> **Unix note.** A single-segment path like `/mcp-jira` is always
|
|
291
|
+
> interpreted as a basename reference. If you have a real `/mcp-jira`
|
|
292
|
+
> directory at the filesystem root, add a trailing slash to force the
|
|
293
|
+
> explicit-path interpretation: `/addproject mj /mcp-jira/`.
|
|
294
|
+
|
|
295
|
+
`/add-project` and `/add_project` are accepted as aliases for `/addproject`.
|
|
296
|
+
|
|
263
297
|
### Listener configuration
|
|
264
298
|
|
|
265
299
|
| Parameter | Default | Description |
|
package/bin/constants.js
CHANGED
|
@@ -8,6 +8,7 @@ export const PLUGINS_DIR = path.join(CLAUDE_DIR, 'plugins');
|
|
|
8
8
|
|
|
9
9
|
// File names
|
|
10
10
|
export const CONFIG_FILENAME = 'claude-notify.config.json';
|
|
11
|
+
export const SEEN_PROJECTS_FILENAME = 'claude-notify.seen.json';
|
|
11
12
|
export const STATE_FILENAME = '.notifier_state.json';
|
|
12
13
|
export const PID_FILENAME = '.listener.pid';
|
|
13
14
|
export const RESOLVER_FILENAME = 'claude-notify-resolve.js';
|
|
@@ -19,6 +20,8 @@ export const PTY_SIGNAL_DIR = path.join(CLAUDE_DIR, 'pty-signals');
|
|
|
19
20
|
|
|
20
21
|
// Full paths
|
|
21
22
|
export const CONFIG_PATH = path.join(CLAUDE_DIR, CONFIG_FILENAME);
|
|
23
|
+
export const SEEN_PROJECTS_PATH = path.join(CLAUDE_DIR, SEEN_PROJECTS_FILENAME);
|
|
24
|
+
export const MAX_SEEN_ENTRIES = 30;
|
|
22
25
|
export const STATE_PATH = path.join(CLAUDE_DIR, STATE_FILENAME);
|
|
23
26
|
export const PID_PATH = path.join(CLAUDE_DIR, PID_FILENAME);
|
|
24
27
|
export const RESOLVER_PATH = path.join(CLAUDE_DIR, RESOLVER_FILENAME);
|
|
@@ -65,6 +68,79 @@ export function saveConfig (config) {
|
|
|
65
68
|
fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2));
|
|
66
69
|
}
|
|
67
70
|
|
|
71
|
+
/**
|
|
72
|
+
* Normalize a filesystem path for equality comparison.
|
|
73
|
+
* - Resolves ".", "..", trailing slashes, mixed separators.
|
|
74
|
+
* - Uses forward slashes.
|
|
75
|
+
* - Lowercases on Windows (case-insensitive FS).
|
|
76
|
+
*/
|
|
77
|
+
export function normalizeForCompare (p) {
|
|
78
|
+
if (!p) {
|
|
79
|
+
return '';
|
|
80
|
+
}
|
|
81
|
+
const resolved = path.resolve(p).replace(/\\/g, '/');
|
|
82
|
+
return process.platform === 'win32' ? resolved.toLowerCase() : resolved;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Load the seen-projects file. Returns { entries: [] } on any error.
|
|
87
|
+
* Schema: { entries: [{ path, basename, lastSeen }] }
|
|
88
|
+
*/
|
|
89
|
+
export function loadSeenProjects () {
|
|
90
|
+
try {
|
|
91
|
+
const raw = fs.readFileSync(SEEN_PROJECTS_PATH, 'utf-8');
|
|
92
|
+
const data = JSON.parse(raw);
|
|
93
|
+
if (data && Array.isArray(data.entries)) {
|
|
94
|
+
return { entries: data.entries };
|
|
95
|
+
}
|
|
96
|
+
} catch {
|
|
97
|
+
// fall through
|
|
98
|
+
}
|
|
99
|
+
return { entries: [] };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Record a folder as "seen" by the notifier. Updates lastSeen in-place
|
|
104
|
+
* if the path already exists, otherwise appends a new entry. Sorts by
|
|
105
|
+
* lastSeen desc and trims to MAX_SEEN_ENTRIES.
|
|
106
|
+
* Silent on errors — never throws.
|
|
107
|
+
*/
|
|
108
|
+
export function recordSeenProject (cwd) {
|
|
109
|
+
try {
|
|
110
|
+
if (!cwd) {
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
const normalized = normalizeForCompare(cwd);
|
|
114
|
+
const data = loadSeenProjects();
|
|
115
|
+
const entries = data.entries;
|
|
116
|
+
|
|
117
|
+
const now = new Date().toISOString();
|
|
118
|
+
const idx = entries.findIndex(
|
|
119
|
+
(e) => normalizeForCompare(e.path) === normalized
|
|
120
|
+
);
|
|
121
|
+
if (idx >= 0) {
|
|
122
|
+
entries[idx].lastSeen = now;
|
|
123
|
+
entries[idx].path = cwd.replace(/\\/g, '/');
|
|
124
|
+
entries[idx].basename = path.basename(cwd);
|
|
125
|
+
} else {
|
|
126
|
+
entries.push({
|
|
127
|
+
path: cwd.replace(/\\/g, '/'),
|
|
128
|
+
basename: path.basename(cwd),
|
|
129
|
+
lastSeen: now,
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
entries.sort((a, b) => (b.lastSeen || '').localeCompare(a.lastSeen || ''));
|
|
134
|
+
const trimmed = entries.slice(0, MAX_SEEN_ENTRIES);
|
|
135
|
+
|
|
136
|
+
const tmp = `${SEEN_PROJECTS_PATH}.${process.pid}.tmp`;
|
|
137
|
+
fs.writeFileSync(tmp, JSON.stringify({ entries: trimmed }, null, 2));
|
|
138
|
+
fs.renameSync(tmp, SEEN_PROJECTS_PATH);
|
|
139
|
+
} catch {
|
|
140
|
+
// silent: notifier must not crash on seen-file errors
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
68
144
|
// Plugin identity
|
|
69
145
|
export const HOOK_COMMAND = 'claude-notify';
|
|
70
146
|
export const MARKETPLACE_KEY = 'bazilio-plugins';
|
package/commit-sha
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
5028775e89c41e96abd523526de5ff59ab1bb1e7
|
|
@@ -674,6 +674,57 @@ Bot: 📂 Projects:
|
|
|
674
674
|
&web → /home/user/projects/web-app
|
|
675
675
|
```
|
|
676
676
|
|
|
677
|
+
### /addproject — register a project alias
|
|
678
|
+
|
|
679
|
+
Registers a new entry in `listener.projects` without manual config editing.
|
|
680
|
+
Two forms for the path argument:
|
|
681
|
+
|
|
682
|
+
```
|
|
683
|
+
You: /addproject mj D:/DEV/FA/_cur/mcp-jira
|
|
684
|
+
Bot: ✅ Project added: &mj → D:/DEV/FA/_cur/mcp-jira
|
|
685
|
+
|
|
686
|
+
You: /addproject mj /mcp-jira
|
|
687
|
+
Bot: ✅ Project added: &mj → D:/DEV/FA/_cur/mcp-jira
|
|
688
|
+
```
|
|
689
|
+
|
|
690
|
+
The basename form (`/name`) resolves via `~/.claude/claude-notify.seen.json`,
|
|
691
|
+
populated by the notifier on every hook. The listener picks the most recent
|
|
692
|
+
entry whose basename equals `name`.
|
|
693
|
+
|
|
694
|
+
Refuses to add when: alias is already taken, path doesn't exist, path is
|
|
695
|
+
not a directory, or the same path is already registered under another
|
|
696
|
+
alias (compared case-insensitively on Windows).
|
|
697
|
+
|
|
698
|
+
Aliases: `/add-project`, `/add_project` work as well (Telegram's menu only
|
|
699
|
+
allows `[a-z0-9_]`, so the canonical short form is `addproject`).
|
|
700
|
+
|
|
701
|
+
### /seen — recent folders seen by notifier
|
|
702
|
+
|
|
703
|
+
Shows the last 30 folders for which the notifier fired, as a monospace
|
|
704
|
+
table. Rows already registered as listener projects display their alias;
|
|
705
|
+
others show `—`.
|
|
706
|
+
|
|
707
|
+
```
|
|
708
|
+
You: /seen
|
|
709
|
+
Bot: 📂 Recent folders (4/30):
|
|
710
|
+
1 mj 2m ago D:/DEV/FA/_cur/mcp-jira
|
|
711
|
+
2 — 15m ago C:/work/side-project
|
|
712
|
+
3 notify 1h ago D:/DEV/FA/_pub/claude-notification-plugin
|
|
713
|
+
4 — 3d ago C:/tmp/old-repo
|
|
714
|
+
```
|
|
715
|
+
|
|
716
|
+
The seen file (`~/.claude/claude-notify.seen.json`) is written atomically
|
|
717
|
+
by the notifier on every hook event; oldest entries are evicted when the
|
|
718
|
+
count exceeds 30.
|
|
719
|
+
|
|
720
|
+
`/seen clear` (alias: `/seen reset`) wipes the file — useful when the
|
|
721
|
+
list is full of stale entries after reorganizing project folders.
|
|
722
|
+
|
|
723
|
+
```
|
|
724
|
+
You: /seen clear
|
|
725
|
+
Bot: ✅ Seen file cleared (17 entries removed).
|
|
726
|
+
```
|
|
727
|
+
|
|
677
728
|
### /worktrees — project worktrees
|
|
678
729
|
|
|
679
730
|
```
|
package/listener/listener.js
CHANGED
|
@@ -11,7 +11,17 @@ import { WorkQueue } from './work-queue.js';
|
|
|
11
11
|
import { PtyRunner } from './pty-runner.js';
|
|
12
12
|
import { WorktreeManager } from './worktree-manager.js';
|
|
13
13
|
import { parseMessage, parseTarget } from './message-parser.js';
|
|
14
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
CLAUDE_DIR,
|
|
16
|
+
CONFIG_PATH,
|
|
17
|
+
LISTENER_LOG_FILENAME,
|
|
18
|
+
MAX_SEEN_ENTRIES,
|
|
19
|
+
SEEN_PROJECTS_PATH,
|
|
20
|
+
getDefaultProject,
|
|
21
|
+
saveConfig,
|
|
22
|
+
normalizeForCompare,
|
|
23
|
+
loadSeenProjects,
|
|
24
|
+
} from '../bin/constants.js';
|
|
15
25
|
import { JsonlReader, resolveJsonlPath, resolveJsonlByMtime } from './jsonl-reader.js';
|
|
16
26
|
|
|
17
27
|
// ----------------------
|
|
@@ -514,6 +524,12 @@ async function handleCommand (cmd, args) {
|
|
|
514
524
|
return handleNewSession(args);
|
|
515
525
|
case '/projects':
|
|
516
526
|
return handleProjects();
|
|
527
|
+
case '/add-project':
|
|
528
|
+
case '/add_project':
|
|
529
|
+
case '/addproject':
|
|
530
|
+
return handleAddProject(args);
|
|
531
|
+
case '/seen':
|
|
532
|
+
return handleSeen(args);
|
|
517
533
|
case '/setdefault':
|
|
518
534
|
return handleSetDefault(args);
|
|
519
535
|
case '/worktrees':
|
|
@@ -825,6 +841,200 @@ function handleSetDefault (args) {
|
|
|
825
841
|
return `✅ Default project: <b>&${escapeHtml(alias)}</b> → <code>${escapeHtml(projPath)}</code>`;
|
|
826
842
|
}
|
|
827
843
|
|
|
844
|
+
// ----------------------
|
|
845
|
+
// /add-project and /seen
|
|
846
|
+
// ----------------------
|
|
847
|
+
|
|
848
|
+
function isBasenameRef (s) {
|
|
849
|
+
// "/foo" — one leading slash + single path segment, no second slash,
|
|
850
|
+
// no backslash, no drive letter. Everything else → explicit path.
|
|
851
|
+
return /^\/[^/\\:]+$/.test(s);
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
function formatAge (iso) {
|
|
855
|
+
if (!iso) {
|
|
856
|
+
return '?';
|
|
857
|
+
}
|
|
858
|
+
const diffMs = Date.now() - new Date(iso).getTime();
|
|
859
|
+
if (Number.isNaN(diffMs)) {
|
|
860
|
+
return '?';
|
|
861
|
+
}
|
|
862
|
+
if (diffMs < 0) {
|
|
863
|
+
return 'now';
|
|
864
|
+
}
|
|
865
|
+
const s = Math.floor(diffMs / 1000);
|
|
866
|
+
if (s < 60) {
|
|
867
|
+
return `${s}s ago`;
|
|
868
|
+
}
|
|
869
|
+
const m = Math.floor(s / 60);
|
|
870
|
+
if (m < 60) {
|
|
871
|
+
return `${m}m ago`;
|
|
872
|
+
}
|
|
873
|
+
const h = Math.floor(m / 60);
|
|
874
|
+
if (h < 24) {
|
|
875
|
+
return `${h}h ago`;
|
|
876
|
+
}
|
|
877
|
+
const d = Math.floor(h / 24);
|
|
878
|
+
if (d < 30) {
|
|
879
|
+
return `${d}d ago`;
|
|
880
|
+
}
|
|
881
|
+
return new Date(iso).toISOString().slice(0, 10);
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
function handleAddProject (args) {
|
|
885
|
+
const trimmed = (args || '').trim();
|
|
886
|
+
const usage = `❌ Usage: /addproject <alias> <path-or-/basename>
|
|
887
|
+
|
|
888
|
+
Examples:
|
|
889
|
+
/addproject mj D:/DEV/FA/_cur/mcp-jira — explicit path
|
|
890
|
+
/addproject mj /mcp-jira — resolve from last notification
|
|
891
|
+
/addproject mj /mcp-jira/ — Unix: literal /mcp-jira directory
|
|
892
|
+
|
|
893
|
+
Aliases for this command: /add-project, /add_project`;
|
|
894
|
+
|
|
895
|
+
if (!trimmed) {
|
|
896
|
+
return usage;
|
|
897
|
+
}
|
|
898
|
+
const parts = trimmed.split(/\s+/);
|
|
899
|
+
if (parts.length < 2) {
|
|
900
|
+
return usage;
|
|
901
|
+
}
|
|
902
|
+
const alias = parts[0];
|
|
903
|
+
const rawTarget = parts.slice(1).join(' ');
|
|
904
|
+
|
|
905
|
+
if (!/^[a-zA-Z0-9_-]+$/.test(alias)) {
|
|
906
|
+
return `❌ Invalid alias "<b>${escapeHtml(alias)}</b>". Allowed: letters, digits, underscore, hyphen.`;
|
|
907
|
+
}
|
|
908
|
+
if (listenerConfig.projects[alias]) {
|
|
909
|
+
return `❌ Alias "<b>${escapeHtml(alias)}</b>" already exists. Use /projects to list.`;
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
// Resolve target → absolute path
|
|
913
|
+
let absPath;
|
|
914
|
+
if (isBasenameRef(rawTarget)) {
|
|
915
|
+
const { entries } = loadSeenProjects();
|
|
916
|
+
const basename = rawTarget.replace(/^\/+/, '');
|
|
917
|
+
const matches = entries
|
|
918
|
+
.filter((e) => e.basename === basename)
|
|
919
|
+
.sort((a, b) => (b.lastSeen || '').localeCompare(a.lastSeen || ''));
|
|
920
|
+
if (matches.length === 0) {
|
|
921
|
+
return `❌ Unknown basename "/${escapeHtml(basename)}". No notification from such folder was seen yet. Use /seen to list recent folders.`;
|
|
922
|
+
}
|
|
923
|
+
absPath = matches[0].path;
|
|
924
|
+
} else {
|
|
925
|
+
absPath = rawTarget;
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
// Validate directory exists
|
|
929
|
+
try {
|
|
930
|
+
if (!fs.statSync(absPath).isDirectory()) {
|
|
931
|
+
return `❌ Path is not a directory: <code>${escapeHtml(absPath)}</code>`;
|
|
932
|
+
}
|
|
933
|
+
} catch {
|
|
934
|
+
return `❌ Path does not exist: <code>${escapeHtml(absPath)}</code>`;
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
// Normalize to forward slashes (match existing config style)
|
|
938
|
+
absPath = absPath.replace(/\\/g, '/');
|
|
939
|
+
|
|
940
|
+
// Check: path already registered under another alias?
|
|
941
|
+
const normalizedNew = normalizeForCompare(absPath);
|
|
942
|
+
for (const [existingAlias, proj] of Object.entries(listenerConfig.projects)) {
|
|
943
|
+
const existingPath = typeof proj === 'string' ? proj : proj?.path;
|
|
944
|
+
if (!existingPath) {
|
|
945
|
+
continue;
|
|
946
|
+
}
|
|
947
|
+
if (normalizeForCompare(existingPath) === normalizedNew) {
|
|
948
|
+
return `❌ Path already registered as <b>&${escapeHtml(existingAlias)}</b> → <code>${escapeHtml(existingPath)}</code>`;
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
// Mutate config + persist
|
|
953
|
+
listenerConfig.projects[alias] = {
|
|
954
|
+
path: absPath,
|
|
955
|
+
claudeArgs: [],
|
|
956
|
+
worktrees: {},
|
|
957
|
+
};
|
|
958
|
+
try {
|
|
959
|
+
saveConfig(config);
|
|
960
|
+
} catch (err) {
|
|
961
|
+
delete listenerConfig.projects[alias];
|
|
962
|
+
logger.error(`Failed to save config: ${err.message}`);
|
|
963
|
+
return `❌ Failed to save config: ${escapeHtml(err.message)}`;
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
// Discover worktrees for the new project (consistency with startup flow)
|
|
967
|
+
try {
|
|
968
|
+
worktreeManager.discoverWorktrees(alias);
|
|
969
|
+
} catch (err) {
|
|
970
|
+
logger.warn(`discoverWorktrees failed for ${alias}: ${err.message}`);
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
logger.info(`Project added: ${alias} → ${absPath}`);
|
|
974
|
+
return `✅ Project added: <b>&${escapeHtml(alias)}</b> → <code>${escapeHtml(absPath)}</code>`;
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
function handleSeen (args) {
|
|
978
|
+
const sub = (args || '').trim().toLowerCase();
|
|
979
|
+
|
|
980
|
+
if (sub === 'clear' || sub === 'reset') {
|
|
981
|
+
let count = 0;
|
|
982
|
+
try {
|
|
983
|
+
const data = loadSeenProjects();
|
|
984
|
+
count = data.entries.length;
|
|
985
|
+
// Atomic overwrite with an empty list
|
|
986
|
+
const tmp = `${SEEN_PROJECTS_PATH}.${process.pid}.tmp`;
|
|
987
|
+
fs.writeFileSync(tmp, JSON.stringify({ entries: [] }, null, 2));
|
|
988
|
+
fs.renameSync(tmp, SEEN_PROJECTS_PATH);
|
|
989
|
+
} catch (err) {
|
|
990
|
+
logger.error(`Failed to clear seen file: ${err.message}`);
|
|
991
|
+
return `❌ Failed to clear seen file: ${escapeHtml(err.message)}`;
|
|
992
|
+
}
|
|
993
|
+
logger.info(`Seen file cleared (${count} entries removed)`);
|
|
994
|
+
return `✅ Seen file cleared (${count} entries removed).`;
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
if (sub && sub !== '') {
|
|
998
|
+
return `❌ Unknown subcommand "<b>${escapeHtml(sub)}</b>". Usage: /seen [clear]`;
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
const { entries } = loadSeenProjects();
|
|
1002
|
+
if (!entries || entries.length === 0) {
|
|
1003
|
+
return 'ℹ No seen folders yet. Notifier will populate this list as you receive notifications.';
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
// Build alias index: normalized project path → alias
|
|
1007
|
+
const aliasByPath = new Map();
|
|
1008
|
+
for (const [alias, proj] of Object.entries(listenerConfig.projects)) {
|
|
1009
|
+
const p = typeof proj === 'string' ? proj : proj?.path;
|
|
1010
|
+
if (p) {
|
|
1011
|
+
aliasByPath.set(normalizeForCompare(p), alias);
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
// Sort by lastSeen desc (defensive — notifier already does this)
|
|
1016
|
+
const sorted = [...entries].sort(
|
|
1017
|
+
(a, b) => (b.lastSeen || '').localeCompare(a.lastSeen || ''),
|
|
1018
|
+
);
|
|
1019
|
+
|
|
1020
|
+
const rows = sorted.map((e, i) => ({
|
|
1021
|
+
num: String(i + 1),
|
|
1022
|
+
alias: aliasByPath.get(normalizeForCompare(e.path)) || '—',
|
|
1023
|
+
age: formatAge(e.lastSeen),
|
|
1024
|
+
projPath: e.path,
|
|
1025
|
+
}));
|
|
1026
|
+
const wNum = Math.max(...rows.map((r) => r.num.length));
|
|
1027
|
+
const wAlias = Math.max(...rows.map((r) => r.alias.length), 5);
|
|
1028
|
+
const wAge = Math.max(...rows.map((r) => r.age.length), 3);
|
|
1029
|
+
|
|
1030
|
+
const lines = rows.map((r) => `${r.num.padStart(wNum)} ${r.alias.padEnd(wAlias)} ${r.age.padStart(wAge)} ${r.projPath}`);
|
|
1031
|
+
|
|
1032
|
+
return {
|
|
1033
|
+
text: `📂 <b>Recent folders</b> (${rows.length}/${MAX_SEEN_ENTRIES}):
|
|
1034
|
+
<pre>${escapeHtml(lines.join('\n'))}</pre>`,
|
|
1035
|
+
};
|
|
1036
|
+
}
|
|
1037
|
+
|
|
828
1038
|
function handleWorktrees (args) {
|
|
829
1039
|
const target = parseTarget(args);
|
|
830
1040
|
if (!target) {
|
|
@@ -1011,6 +1221,9 @@ function handleHelp () {
|
|
|
1011
1221
|
/clear &project[/branch] — clear queue + reset session
|
|
1012
1222
|
/newsession [&project[/branch]] — reset session (keep queue)
|
|
1013
1223
|
/projects — list projects
|
|
1224
|
+
/addproject <alias> <path-or-/basename> — register a project
|
|
1225
|
+
/seen — recent folders seen by notifier
|
|
1226
|
+
/seen clear — wipe the seen list
|
|
1014
1227
|
/setdefault — change default project
|
|
1015
1228
|
/worktrees &project — project worktrees
|
|
1016
1229
|
/worktree &project/branch — create worktree
|
|
@@ -1163,6 +1376,8 @@ async function mainLoop () {
|
|
|
1163
1376
|
{ command: 'status', description: 'Status of all projects' },
|
|
1164
1377
|
{ command: 'queue', description: 'Show all queues' },
|
|
1165
1378
|
{ command: 'projects', description: 'List projects' },
|
|
1379
|
+
{ command: 'addproject', description: 'Register a project alias' },
|
|
1380
|
+
{ command: 'seen', description: 'Recent folders seen by notifier' },
|
|
1166
1381
|
{ command: 'setdefault', description: 'Change default project' },
|
|
1167
1382
|
{ command: 'history', description: 'Recent task history' },
|
|
1168
1383
|
{ command: 'pty', description: 'PTY session diagnostics' },
|
package/notifier/notifier.js
CHANGED
|
@@ -4,7 +4,13 @@ import fs from 'fs';
|
|
|
4
4
|
import path from 'path';
|
|
5
5
|
import process from 'process';
|
|
6
6
|
import { execSync, spawn } from 'child_process';
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
CONFIG_PATH,
|
|
9
|
+
STATE_PATH,
|
|
10
|
+
PTY_SIGNAL_DIR,
|
|
11
|
+
normalizeForCompare,
|
|
12
|
+
recordSeenProject,
|
|
13
|
+
} from '../bin/constants.js';
|
|
8
14
|
|
|
9
15
|
// ----------------------
|
|
10
16
|
// CONFIG
|
|
@@ -26,22 +32,18 @@ function debugLog (config, ...args) {
|
|
|
26
32
|
}
|
|
27
33
|
}
|
|
28
34
|
|
|
29
|
-
function normalizePath (p) {
|
|
30
|
-
return path.resolve(p).replace(/\\/g, '/').toLowerCase();
|
|
31
|
-
}
|
|
32
|
-
|
|
33
35
|
function resolveProjectName (cwd, config) {
|
|
34
36
|
const fallback = path.basename(cwd);
|
|
35
37
|
const projects = config?.listenerProjects;
|
|
36
38
|
if (!projects || typeof projects !== 'object') {
|
|
37
39
|
return fallback;
|
|
38
40
|
}
|
|
39
|
-
const normalizedCwd =
|
|
41
|
+
const normalizedCwd = normalizeForCompare(cwd);
|
|
40
42
|
for (const entry of Object.values(projects)) {
|
|
41
43
|
if (!entry?.path) {
|
|
42
44
|
continue;
|
|
43
45
|
}
|
|
44
|
-
if (normalizedCwd ===
|
|
46
|
+
if (normalizedCwd === normalizeForCompare(entry.path) && entry.name) {
|
|
45
47
|
return entry.name;
|
|
46
48
|
}
|
|
47
49
|
}
|
|
@@ -748,6 +750,10 @@ process.stdin.on('end', async () => {
|
|
|
748
750
|
const project = resolveProjectName(cwd, config);
|
|
749
751
|
const sessionId = event.session_id || 'default';
|
|
750
752
|
|
|
753
|
+
// Record this cwd in the seen-projects file for /add-project /basename
|
|
754
|
+
// resolution and the /seen listener command. Silent on errors.
|
|
755
|
+
recordSeenProject(cwd);
|
|
756
|
+
|
|
751
757
|
const disabled = isNotifierDisabled();
|
|
752
758
|
if (disabled === true) {
|
|
753
759
|
process.exit(0);
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-notification-plugin",
|
|
3
3
|
"productName": "claude-notification-plugin",
|
|
4
|
-
"version": "1.1.
|
|
4
|
+
"version": "1.1.81",
|
|
5
5
|
"description": "Claude Code task-completion notifications: Telegram, desktop notifications (Windows/macOS/Linux), sound, and voice",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"engines": {
|