@wipcomputer/wip-ldm-os 0.2.14 → 0.3.2

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/SKILL.md CHANGED
@@ -5,7 +5,7 @@ license: MIT
5
5
  interface: [cli, skill]
6
6
  metadata:
7
7
  display-name: "LDM OS"
8
- version: "0.2.14"
8
+ version: "0.3.2"
9
9
  homepage: "https://github.com/wipcomputer/wip-ldm-os"
10
10
  author: "Parker Todd Brooks"
11
11
  category: infrastructure
package/bin/ldm.js CHANGED
@@ -9,6 +9,11 @@
9
9
  * ldm install Install/update all registered components
10
10
  * ldm doctor Check health of all extensions
11
11
  * ldm status Show LDM OS version and extension count
12
+ * ldm sessions List active sessions
13
+ * ldm msg send <to> <b> Send a message to a session
14
+ * ldm msg list List pending messages
15
+ * ldm msg broadcast <b> Send to all sessions
16
+ * ldm updates Show available updates
12
17
  * ldm --version Show version
13
18
  */
14
19
 
@@ -47,6 +52,8 @@ const JSON_OUTPUT = args.includes('--json');
47
52
  const YES_FLAG = args.includes('--yes') || args.includes('-y');
48
53
  const NONE_FLAG = args.includes('--none');
49
54
  const FIX_FLAG = args.includes('--fix');
55
+ const CLEANUP_FLAG = args.includes('--cleanup');
56
+ const CHECK_FLAG = args.includes('--check');
50
57
 
51
58
  function readJSON(path) {
52
59
  try {
@@ -61,6 +68,68 @@ function writeJSON(path, data) {
61
68
  writeFileSync(path, JSON.stringify(data, null, 2) + '\n');
62
69
  }
63
70
 
71
+ // ── CLI version check (#29) ──
72
+
73
+ function checkCliVersion() {
74
+ try {
75
+ const result = execSync('npm view @wipcomputer/wip-ldm-os version 2>/dev/null', {
76
+ encoding: 'utf8',
77
+ timeout: 10000,
78
+ }).trim();
79
+ if (result && result !== PKG_VERSION) {
80
+ console.log('');
81
+ console.log(` CLI is outdated: v${PKG_VERSION} installed, v${result} available.`);
82
+ console.log(` Run: npm install -g @wipcomputer/wip-ldm-os@${result}`);
83
+ }
84
+ } catch {
85
+ // npm check failed, skip silently
86
+ }
87
+ }
88
+
89
+ // ── Stale hook cleanup (#30) ──
90
+
91
+ function cleanStaleHooks() {
92
+ const settingsPath = join(HOME, '.claude', 'settings.json');
93
+ const settings = readJSON(settingsPath);
94
+ if (!settings?.hooks) return 0;
95
+
96
+ let cleaned = 0;
97
+
98
+ for (const [event, hookGroups] of Object.entries(settings.hooks)) {
99
+ if (!Array.isArray(hookGroups)) continue;
100
+
101
+ // Filter out hook groups where ALL hooks point to non-existent paths
102
+ const original = hookGroups.length;
103
+ settings.hooks[event] = hookGroups.filter(group => {
104
+ const hooks = group.hooks || [];
105
+ if (hooks.length === 0) return true; // keep empty groups (matcher-only)
106
+
107
+ // Check each hook command for stale paths
108
+ const liveHooks = hooks.filter(h => {
109
+ if (!h.command) return true;
110
+ // Extract the path from "node /path/to/file.mjs" or "node \"/path/to/file.mjs\""
111
+ const match = h.command.match(/node\s+"?([^"]+)"?\s*$/);
112
+ if (!match) return true; // keep non-node commands
113
+ const scriptPath = match[1];
114
+ if (existsSync(scriptPath)) return true;
115
+ console.log(` + Removed stale hook: ${event} -> ${scriptPath}`);
116
+ cleaned++;
117
+ return false;
118
+ });
119
+
120
+ // Keep the group if it still has live hooks
121
+ group.hooks = liveHooks;
122
+ return liveHooks.length > 0;
123
+ });
124
+ }
125
+
126
+ if (cleaned > 0) {
127
+ writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
128
+ }
129
+
130
+ return cleaned;
131
+ }
132
+
64
133
  // ── Catalog helpers ──
65
134
 
66
135
  function loadCatalog() {
@@ -83,7 +152,10 @@ async function cmdInit() {
83
152
  join(LDM_ROOT, 'agents'),
84
153
  join(LDM_ROOT, 'memory'),
85
154
  join(LDM_ROOT, 'state'),
155
+ join(LDM_ROOT, 'sessions'),
156
+ join(LDM_ROOT, 'messages'),
86
157
  join(LDM_ROOT, 'shared', 'boot'),
158
+ join(LDM_ROOT, 'shared', 'cron'),
87
159
  ];
88
160
 
89
161
  const existing = existsSync(VERSION_PATH);
@@ -558,6 +630,10 @@ async function cmdInstallCatalog() {
558
630
 
559
631
  console.log('');
560
632
  console.log(` Updated ${updated}/${updatable.length} extension(s).`);
633
+
634
+ // Check if CLI itself is outdated (#29)
635
+ checkCliVersion();
636
+
561
637
  console.log('');
562
638
  }
563
639
 
@@ -631,11 +707,21 @@ async function cmdDoctor() {
631
707
  }
632
708
  }
633
709
 
710
+ // --fix: clean stale hook paths in settings.json (#30)
711
+ if (FIX_FLAG) {
712
+ const hooksCleaned = cleanStaleHooks();
713
+ if (hooksCleaned > 0) {
714
+ issues = Math.max(0, issues - hooksCleaned);
715
+ }
716
+ }
717
+
634
718
  // 4. Check sacred locations
635
719
  const sacred = [
636
720
  { path: join(LDM_ROOT, 'memory'), label: 'memory/' },
637
721
  { path: join(LDM_ROOT, 'agents'), label: 'agents/' },
638
722
  { path: join(LDM_ROOT, 'state'), label: 'state/' },
723
+ { path: join(LDM_ROOT, 'sessions'), label: 'sessions/' },
724
+ { path: join(LDM_ROOT, 'messages'), label: 'messages/' },
639
725
  ];
640
726
 
641
727
  for (const s of sacred) {
@@ -653,6 +739,29 @@ async function cmdDoctor() {
653
739
  if (settings?.hooks) {
654
740
  const hookCount = Object.values(settings.hooks).reduce((sum, arr) => sum + (arr?.length || 0), 0);
655
741
  console.log(` + Claude Code hooks: ${hookCount} configured`);
742
+
743
+ // Check for stale hook paths
744
+ let staleHooks = 0;
745
+ for (const [event, hookGroups] of Object.entries(settings.hooks)) {
746
+ if (!Array.isArray(hookGroups)) continue;
747
+ for (const group of hookGroups) {
748
+ for (const h of (group.hooks || [])) {
749
+ if (!h.command) continue;
750
+ const match = h.command.match(/node\s+"?([^"]+)"?\s*$/);
751
+ if (!match) continue;
752
+ if (!existsSync(match[1])) {
753
+ staleHooks++;
754
+ if (!FIX_FLAG) {
755
+ console.log(` ! Stale hook: ${event} -> ${match[1]}`);
756
+ }
757
+ }
758
+ }
759
+ }
760
+ }
761
+ if (staleHooks > 0 && !FIX_FLAG) {
762
+ console.log(` Run: ldm doctor --fix to clean ${staleHooks} stale hook(s)`);
763
+ issues += staleHooks;
764
+ }
656
765
  } else {
657
766
  console.log(` - Claude Code hooks: none configured`);
658
767
  }
@@ -716,6 +825,218 @@ function cmdStatus() {
716
825
  console.log('');
717
826
  }
718
827
 
828
+ // ── ldm sessions ──
829
+
830
+ async function cmdSessions() {
831
+ const { listSessions } = await import('../lib/sessions.mjs');
832
+ const sessions = listSessions({ includeStale: CLEANUP_FLAG });
833
+
834
+ if (CLEANUP_FLAG) {
835
+ // listSessions already cleans stale when includeStale is false.
836
+ // With --cleanup, we list stale ones so user can see them, then re-run without stale.
837
+ const stale = sessions.filter(s => !s.alive);
838
+ if (stale.length > 0) {
839
+ const { deregisterSession } = await import('../lib/sessions.mjs');
840
+ for (const s of stale) {
841
+ deregisterSession(s.name);
842
+ }
843
+ console.log(` Cleaned ${stale.length} stale session(s).`);
844
+ } else {
845
+ console.log(' No stale sessions found.');
846
+ }
847
+ console.log('');
848
+ return;
849
+ }
850
+
851
+ const live = sessions.filter(s => s.alive);
852
+
853
+ if (JSON_OUTPUT) {
854
+ console.log(JSON.stringify(live, null, 2));
855
+ return;
856
+ }
857
+
858
+ console.log('');
859
+ console.log(' Active Sessions');
860
+ console.log(' ────────────────────────────────────');
861
+
862
+ if (live.length === 0) {
863
+ console.log(' No active sessions.');
864
+ } else {
865
+ for (const s of live) {
866
+ const age = timeSince(s.startTime);
867
+ console.log(` ${s.name} agent=${s.agentId} pid=${s.pid} up=${age}`);
868
+ }
869
+ }
870
+
871
+ console.log('');
872
+ }
873
+
874
+ function timeSince(isoString) {
875
+ try {
876
+ const ms = Date.now() - new Date(isoString).getTime();
877
+ const secs = Math.floor(ms / 1000);
878
+ if (secs < 60) return `${secs}s`;
879
+ const mins = Math.floor(secs / 60);
880
+ if (mins < 60) return `${mins}m`;
881
+ const hours = Math.floor(mins / 60);
882
+ if (hours < 24) return `${hours}h ${mins % 60}m`;
883
+ const days = Math.floor(hours / 24);
884
+ return `${days}d ${hours % 24}h`;
885
+ } catch {
886
+ return '?';
887
+ }
888
+ }
889
+
890
+ // ── ldm msg ──
891
+
892
+ async function cmdMsg() {
893
+ const subcommand = args[1];
894
+
895
+ if (!subcommand || subcommand === 'list') {
896
+ return cmdMsgList();
897
+ }
898
+ if (subcommand === 'send') {
899
+ return cmdMsgSend();
900
+ }
901
+ if (subcommand === 'broadcast') {
902
+ return cmdMsgBroadcast();
903
+ }
904
+
905
+ console.error(` Unknown msg subcommand: ${subcommand}`);
906
+ console.error(' Usage: ldm msg [send <to> <body> | list | broadcast <body>]');
907
+ process.exit(1);
908
+ }
909
+
910
+ async function cmdMsgList() {
911
+ const { readMessages } = await import('../lib/messages.mjs');
912
+ const sessionName = process.env.CLAUDE_SESSION_NAME || 'unknown';
913
+ const messages = readMessages(sessionName, { markRead: false });
914
+
915
+ if (JSON_OUTPUT) {
916
+ console.log(JSON.stringify(messages, null, 2));
917
+ return;
918
+ }
919
+
920
+ console.log('');
921
+ console.log(` Messages for "${sessionName}"`);
922
+ console.log(' ────────────────────────────────────');
923
+
924
+ if (messages.length === 0) {
925
+ console.log(' No pending messages.');
926
+ } else {
927
+ for (const m of messages) {
928
+ const ts = m.timestamp?.split('T')[1]?.split('.')[0] || '';
929
+ console.log(` [${m.type}] ${ts} from=${m.from}: ${m.body}`);
930
+ }
931
+ }
932
+
933
+ console.log('');
934
+ }
935
+
936
+ async function cmdMsgSend() {
937
+ const { sendMessage } = await import('../lib/messages.mjs');
938
+ // args: ['msg', 'send', '<to>', '<body...>']
939
+ const to = args[2];
940
+ const body = args.slice(3).filter(a => !a.startsWith('--')).join(' ');
941
+
942
+ if (!to || !body) {
943
+ console.error(' Usage: ldm msg send <to> <body>');
944
+ process.exit(1);
945
+ }
946
+
947
+ const sessionName = process.env.CLAUDE_SESSION_NAME || 'ldm-cli';
948
+ const id = sendMessage({ from: sessionName, to, body, type: 'chat' });
949
+
950
+ if (id) {
951
+ console.log(` Message sent to "${to}" (id: ${id})`);
952
+ } else {
953
+ console.error(' x Failed to send message.');
954
+ process.exit(1);
955
+ }
956
+ }
957
+
958
+ async function cmdMsgBroadcast() {
959
+ const { sendMessage } = await import('../lib/messages.mjs');
960
+ // args: ['msg', 'broadcast', '<body...>']
961
+ const body = args.slice(2).filter(a => !a.startsWith('--')).join(' ');
962
+
963
+ if (!body) {
964
+ console.error(' Usage: ldm msg broadcast <body>');
965
+ process.exit(1);
966
+ }
967
+
968
+ const sessionName = process.env.CLAUDE_SESSION_NAME || 'ldm-cli';
969
+ const id = sendMessage({ from: sessionName, to: 'all', body, type: 'chat' });
970
+
971
+ if (id) {
972
+ console.log(` Broadcast sent (id: ${id})`);
973
+ } else {
974
+ console.error(' x Failed to send broadcast.');
975
+ process.exit(1);
976
+ }
977
+ }
978
+
979
+ // ── ldm updates ──
980
+
981
+ async function cmdUpdates() {
982
+ if (CHECK_FLAG) {
983
+ // Re-check npm registry
984
+ const { checkForUpdates } = await import('../lib/updates.mjs');
985
+ console.log(' Checking npm for updates...');
986
+ console.log('');
987
+ const result = checkForUpdates();
988
+
989
+ if (JSON_OUTPUT) {
990
+ console.log(JSON.stringify(result, null, 2));
991
+ return;
992
+ }
993
+
994
+ if (result.updatesAvailable === 0) {
995
+ console.log(` Checked ${result.checked} extensions. Everything is up to date.`);
996
+ } else {
997
+ console.log(` Checked ${result.checked} extensions. ${result.updatesAvailable} update(s) available:`);
998
+ console.log('');
999
+ for (const u of result.updates) {
1000
+ console.log(` ${u.name}: ${u.currentVersion} -> ${u.latestVersion} (${u.packageName})`);
1001
+ }
1002
+ console.log('');
1003
+ console.log(' Run: ldm install');
1004
+ }
1005
+ console.log('');
1006
+ return;
1007
+ }
1008
+
1009
+ // Show cached results
1010
+ const { readUpdateManifest } = await import('../lib/updates.mjs');
1011
+ const manifest = readUpdateManifest();
1012
+
1013
+ if (JSON_OUTPUT) {
1014
+ console.log(JSON.stringify(manifest || {}, null, 2));
1015
+ return;
1016
+ }
1017
+
1018
+ console.log('');
1019
+ console.log(' Available Updates');
1020
+ console.log(' ────────────────────────────────────');
1021
+
1022
+ if (!manifest) {
1023
+ console.log(' No update check has been run yet.');
1024
+ console.log(' Run: ldm updates --check');
1025
+ } else if (manifest.updatesAvailable === 0) {
1026
+ console.log(` Everything is up to date. (checked ${manifest.checkedAt?.split('T')[0] || 'unknown'})`);
1027
+ } else {
1028
+ console.log(` ${manifest.updatesAvailable} update(s) available (checked ${manifest.checkedAt?.split('T')[0] || 'unknown'}):`);
1029
+ console.log('');
1030
+ for (const u of manifest.updates) {
1031
+ console.log(` ${u.name}: ${u.currentVersion} -> ${u.latestVersion}`);
1032
+ }
1033
+ console.log('');
1034
+ console.log(' Run: ldm install');
1035
+ }
1036
+
1037
+ console.log('');
1038
+ }
1039
+
719
1040
  // ── Main ──
720
1041
 
721
1042
  async function main() {
@@ -730,10 +1051,19 @@ async function main() {
730
1051
  console.log(' ldm install Update all registered extensions');
731
1052
  console.log(' ldm doctor Check health of all extensions');
732
1053
  console.log(' ldm status Show version and extension list');
1054
+ console.log(' ldm sessions List active sessions');
1055
+ console.log(' ldm sessions --cleanup Remove stale session entries');
1056
+ console.log(' ldm msg send <to> <body> Send a message to a session');
1057
+ console.log(' ldm msg list List pending messages');
1058
+ console.log(' ldm msg broadcast <body> Send to all sessions');
1059
+ console.log(' ldm updates Show available updates from cache');
1060
+ console.log(' ldm updates --check Re-check npm registry for updates');
733
1061
  console.log('');
734
1062
  console.log(' Flags:');
735
1063
  console.log(' --dry-run Show what would happen without making changes');
736
1064
  console.log(' --json Output results as JSON');
1065
+ console.log(' --cleanup Remove stale entries (sessions)');
1066
+ console.log(' --check Re-check registry (updates)');
737
1067
  console.log('');
738
1068
  console.log(' Interfaces detected:');
739
1069
  console.log(' CLI ... package.json bin -> npm install -g');
@@ -770,6 +1100,15 @@ async function main() {
770
1100
  case 'status':
771
1101
  cmdStatus();
772
1102
  break;
1103
+ case 'sessions':
1104
+ await cmdSessions();
1105
+ break;
1106
+ case 'msg':
1107
+ await cmdMsg();
1108
+ break;
1109
+ case 'updates':
1110
+ await cmdUpdates();
1111
+ break;
773
1112
  default:
774
1113
  console.error(` Unknown command: ${command}`);
775
1114
  console.error(` Run: ldm --help`);