myceliumail 1.0.9 → 1.0.13

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.
Files changed (96) hide show
  1. package/.mappersan/outbox.json +15 -0
  2. package/.spidersan/registry.json +39 -0
  3. package/CHANGELOG.md +29 -0
  4. package/CLAUDE.md +29 -0
  5. package/README.md +23 -2
  6. package/dist/bin/myceliumail.js +17 -1
  7. package/dist/bin/myceliumail.js.map +1 -1
  8. package/dist/commands/close.d.ts +9 -0
  9. package/dist/commands/close.d.ts.map +1 -0
  10. package/dist/commands/close.js +153 -0
  11. package/dist/commands/close.js.map +1 -0
  12. package/dist/commands/collab.d.ts +8 -0
  13. package/dist/commands/collab.d.ts.map +1 -0
  14. package/dist/commands/collab.js +112 -0
  15. package/dist/commands/collab.js.map +1 -0
  16. package/dist/commands/inbox.d.ts.map +1 -1
  17. package/dist/commands/inbox.js +105 -26
  18. package/dist/commands/inbox.js.map +1 -1
  19. package/dist/commands/tags.d.ts +6 -0
  20. package/dist/commands/tags.d.ts.map +1 -0
  21. package/dist/commands/tags.js +90 -0
  22. package/dist/commands/tags.js.map +1 -0
  23. package/dist/commands/wake.d.ts +9 -0
  24. package/dist/commands/wake.d.ts.map +1 -0
  25. package/dist/commands/wake.js +198 -0
  26. package/dist/commands/wake.js.map +1 -0
  27. package/dist/dashboard/public/app.js +117 -0
  28. package/dist/dashboard/public/index.html +63 -5
  29. package/dist/dashboard/routes.d.ts.map +1 -1
  30. package/dist/dashboard/routes.js +31 -2
  31. package/dist/dashboard/routes.js.map +1 -1
  32. package/dist/lib/update-check.d.ts.map +1 -1
  33. package/dist/lib/update-check.js +6 -4
  34. package/dist/lib/update-check.js.map +1 -1
  35. package/dist/lib/watson-digest.d.ts +40 -0
  36. package/dist/lib/watson-digest.d.ts.map +1 -0
  37. package/dist/lib/watson-digest.js +164 -0
  38. package/dist/lib/watson-digest.js.map +1 -0
  39. package/dist/storage/supabase.d.ts +4 -0
  40. package/dist/storage/supabase.d.ts.map +1 -1
  41. package/dist/storage/supabase.js +57 -0
  42. package/dist/storage/supabase.js.map +1 -1
  43. package/docs/COLLAB_mappersan_mycmail_setup.md +115 -0
  44. package/docs/COLLAB_wake_close_commands.md +518 -0
  45. package/docs/CROSS_TOOL_INTEGRATION_PLAN.md +246 -0
  46. package/docs/JSON_SCHEMA_WAKE_CLOSE.md +246 -0
  47. package/docs/MYCMAIL_QUICKSTART.md +103 -0
  48. package/docs/WAKE_AGENTS_SHARED_DOC.md +1215 -0
  49. package/mcp-server/README.md +75 -69
  50. package/mcp-server/package-lock.json +2 -2
  51. package/mcp-server/package.json +5 -1
  52. package/mcp-server/postinstall.js +14 -0
  53. package/mcp-server/src/server.ts +39 -0
  54. package/mobile-app/README.md +36 -0
  55. package/mobile-app/app/compose/page.tsx +140 -0
  56. package/mobile-app/app/favicon.ico +0 -0
  57. package/mobile-app/app/globals.css +26 -0
  58. package/mobile-app/app/layout.tsx +42 -0
  59. package/mobile-app/app/message/[id]/page.tsx +126 -0
  60. package/mobile-app/app/page.tsx +131 -0
  61. package/mobile-app/components/MessageCard.tsx +60 -0
  62. package/mobile-app/eslint.config.mjs +18 -0
  63. package/mobile-app/lib/supabase.ts +87 -0
  64. package/mobile-app/next.config.ts +7 -0
  65. package/mobile-app/package-lock.json +6674 -0
  66. package/mobile-app/package.json +27 -0
  67. package/mobile-app/postcss.config.mjs +7 -0
  68. package/mobile-app/public/file.svg +1 -0
  69. package/mobile-app/public/globe.svg +1 -0
  70. package/mobile-app/public/next.svg +1 -0
  71. package/mobile-app/public/vercel.svg +1 -0
  72. package/mobile-app/public/window.svg +1 -0
  73. package/mobile-app/tsconfig.json +34 -0
  74. package/package.json +2 -1
  75. package/postinstall.js +14 -0
  76. package/src/bin/myceliumail.ts +19 -1
  77. package/src/commands/close.ts +172 -0
  78. package/src/commands/collab.ts +125 -0
  79. package/src/commands/inbox.ts +120 -29
  80. package/src/commands/tags.ts +102 -0
  81. package/src/commands/wake.ts +228 -0
  82. package/src/dashboard/public/app.js +117 -0
  83. package/src/dashboard/public/index.html +63 -5
  84. package/src/dashboard/routes.ts +31 -2
  85. package/src/lib/update-check.ts +7 -4
  86. package/src/lib/watson-digest.ts +217 -0
  87. package/src/storage/supabase.ts +71 -0
  88. package/vscode-extension/README.md +107 -0
  89. package/vscode-extension/package-lock.json +1941 -0
  90. package/vscode-extension/package.json +117 -0
  91. package/vscode-extension/src/chatParticipant.ts +179 -0
  92. package/vscode-extension/src/extension.ts +262 -0
  93. package/vscode-extension/src/handlers.ts +265 -0
  94. package/vscode-extension/src/realtime.ts +302 -0
  95. package/vscode-extension/src/types.ts +41 -0
  96. package/vscode-extension/tsconfig.json +26 -0
@@ -0,0 +1,27 @@
1
+ {
2
+ "name": "mobile-app",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "scripts": {
6
+ "dev": "next dev",
7
+ "build": "next build",
8
+ "start": "next start",
9
+ "lint": "eslint"
10
+ },
11
+ "dependencies": {
12
+ "@supabase/supabase-js": "^2.89.0",
13
+ "next": "16.1.1",
14
+ "react": "19.2.3",
15
+ "react-dom": "19.2.3"
16
+ },
17
+ "devDependencies": {
18
+ "@tailwindcss/postcss": "^4",
19
+ "@types/node": "^20",
20
+ "@types/react": "^19",
21
+ "@types/react-dom": "^19",
22
+ "eslint": "^9",
23
+ "eslint-config-next": "16.1.1",
24
+ "tailwindcss": "^4",
25
+ "typescript": "^5"
26
+ }
27
+ }
@@ -0,0 +1,7 @@
1
+ const config = {
2
+ plugins: {
3
+ "@tailwindcss/postcss": {},
4
+ },
5
+ };
6
+
7
+ export default config;
@@ -0,0 +1 @@
1
+ <svg fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M14.5 13.5V5.41a1 1 0 0 0-.3-.7L9.8.29A1 1 0 0 0 9.08 0H1.5v13.5A2.5 2.5 0 0 0 4 16h8a2.5 2.5 0 0 0 2.5-2.5m-1.5 0v-7H8v-5H3v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1M9.5 5V2.12L12.38 5zM5.13 5h-.62v1.25h2.12V5zm-.62 3h7.12v1.25H4.5zm.62 3h-.62v1.25h7.12V11z" clip-rule="evenodd" fill="#666" fill-rule="evenodd"/></svg>
@@ -0,0 +1 @@
1
+ <svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g clip-path="url(#a)"><path fill-rule="evenodd" clip-rule="evenodd" d="M10.27 14.1a6.5 6.5 0 0 0 3.67-3.45q-1.24.21-2.7.34-.31 1.83-.97 3.1M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16m.48-1.52a7 7 0 0 1-.96 0H7.5a4 4 0 0 1-.84-1.32q-.38-.89-.63-2.08a40 40 0 0 0 3.92 0q-.25 1.2-.63 2.08a4 4 0 0 1-.84 1.31zm2.94-4.76q1.66-.15 2.95-.43a7 7 0 0 0 0-2.58q-1.3-.27-2.95-.43a18 18 0 0 1 0 3.44m-1.27-3.54a17 17 0 0 1 0 3.64 39 39 0 0 1-4.3 0 17 17 0 0 1 0-3.64 39 39 0 0 1 4.3 0m1.1-1.17q1.45.13 2.69.34a6.5 6.5 0 0 0-3.67-3.44q.65 1.26.98 3.1M8.48 1.5l.01.02q.41.37.84 1.31.38.89.63 2.08a40 40 0 0 0-3.92 0q.25-1.2.63-2.08a4 4 0 0 1 .85-1.32 7 7 0 0 1 .96 0m-2.75.4a6.5 6.5 0 0 0-3.67 3.44 29 29 0 0 1 2.7-.34q.31-1.83.97-3.1M4.58 6.28q-1.66.16-2.95.43a7 7 0 0 0 0 2.58q1.3.27 2.95.43a18 18 0 0 1 0-3.44m.17 4.71q-1.45-.12-2.69-.34a6.5 6.5 0 0 0 3.67 3.44q-.65-1.27-.98-3.1" fill="#666"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>
@@ -0,0 +1 @@
1
+ <svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1155 1000"><path d="m577.3 0 577.4 1000H0z" fill="#fff"/></svg>
@@ -0,0 +1 @@
1
+ <svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 2.5h13v10a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1zM0 1h16v11.5a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5zm3.75 4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5M7 4.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0m1.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5" fill="#666"/></svg>
@@ -0,0 +1,34 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2017",
4
+ "lib": ["dom", "dom.iterable", "esnext"],
5
+ "allowJs": true,
6
+ "skipLibCheck": true,
7
+ "strict": true,
8
+ "noEmit": true,
9
+ "esModuleInterop": true,
10
+ "module": "esnext",
11
+ "moduleResolution": "bundler",
12
+ "resolveJsonModule": true,
13
+ "isolatedModules": true,
14
+ "jsx": "react-jsx",
15
+ "incremental": true,
16
+ "plugins": [
17
+ {
18
+ "name": "next"
19
+ }
20
+ ],
21
+ "paths": {
22
+ "@/*": ["./*"]
23
+ }
24
+ },
25
+ "include": [
26
+ "next-env.d.ts",
27
+ "**/*.ts",
28
+ "**/*.tsx",
29
+ ".next/types/**/*.ts",
30
+ ".next/dev/types/**/*.ts",
31
+ "**/*.mts"
32
+ ],
33
+ "exclude": ["node_modules"]
34
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "myceliumail",
3
- "version": "1.0.9",
3
+ "version": "1.0.13",
4
4
  "description": "End-to-End Encrypted Messaging for AI Agents",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",
@@ -11,6 +11,7 @@
11
11
  "scripts": {
12
12
  "build": "tsc",
13
13
  "postbuild": "cp -r src/dashboard/public dist/dashboard/",
14
+ "postinstall": "node postinstall.js 2>/dev/null || true",
14
15
  "dev": "tsc --watch",
15
16
  "test": "vitest run",
16
17
  "test:watch": "vitest",
package/postinstall.js ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env node
2
+ // Write to stderr so it shows even during npm install
3
+ process.stderr.write(`
4
+ \x1b[36m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\x1b[0m
5
+ \x1b[33m🍄 Thanks for installing Myceliumail!\x1b[0m
6
+
7
+ We'd love your feedback on your experience!
8
+
9
+ \x1b[32m📧 Beta testing & collaborations:\x1b[0m treebird@treebird.dev
10
+ \x1b[32m☕ Support the project:\x1b[0m https://buymeacoffee.com/tree.bird
11
+
12
+ This is early-stage software built in public. Your feedback helps!
13
+ \x1b[36m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\x1b[0m
14
+ `);
@@ -10,8 +10,16 @@
10
10
  import 'dotenv/config';
11
11
 
12
12
  import { Command } from 'commander';
13
+ import { readFileSync } from 'fs';
14
+ import { fileURLToPath } from 'url';
15
+ import { dirname, join } from 'path';
13
16
  import { loadConfig } from '../lib/config.js';
14
17
 
18
+ // Get version from package.json
19
+ const __filename = fileURLToPath(import.meta.url);
20
+ const __dirname = dirname(__filename);
21
+ const pkg = JSON.parse(readFileSync(join(__dirname, '../../package.json'), 'utf-8'));
22
+
15
23
  // Import commands
16
24
  import { createKeygenCommand } from '../commands/keygen.js';
17
25
  import { createKeysCommand } from '../commands/keys.js';
@@ -26,6 +34,10 @@ import { createWatchCommand } from '../commands/watch.js';
26
34
  import { createExportCommand } from '../commands/export.js';
27
35
  import { createStatusCommand } from '../commands/status.js';
28
36
  import { createActivateCommand, createLicenseStatusCommand } from '../commands/activate.js';
37
+ import { createWakeCommand } from '../commands/wake.js';
38
+ import { createCloseCommand } from '../commands/close.js';
39
+ import { createTagsCommand } from '../commands/tags.js';
40
+ import { createCollabCommand } from '../commands/collab.js';
29
41
  import { checkForUpdates } from '../lib/update-check.js';
30
42
 
31
43
  const program = new Command();
@@ -33,7 +45,7 @@ const program = new Command();
33
45
  program
34
46
  .name('mycmail')
35
47
  .description('🍄 Myceliumail - End-to-End Encrypted Messaging for AI Agents')
36
- .version('1.0.0');
48
+ .version(pkg.version);
37
49
 
38
50
  // Show current agent in help
39
51
  const config = loadConfig();
@@ -56,6 +68,12 @@ program.addCommand(createWatchCommand());
56
68
  program.addCommand(createExportCommand());
57
69
  program.addCommand(createStatusCommand());
58
70
 
71
+ // Session lifecycle commands
72
+ program.addCommand(createWakeCommand());
73
+ program.addCommand(createCloseCommand());
74
+ program.addCommand(createTagsCommand());
75
+ program.addCommand(createCollabCommand());
76
+
59
77
  // License management
60
78
  program.addCommand(createActivateCommand());
61
79
  program.addCommand(createLicenseStatusCommand());
@@ -0,0 +1,172 @@
1
+ /**
2
+ * close command - End a session properly
3
+ *
4
+ * Broadcasts signing-off message and updates session state.
5
+ * Designed for agent session lifecycle management.
6
+ */
7
+
8
+ import { Command } from 'commander';
9
+ import { loadConfig } from '../lib/config.js';
10
+ import { generateDigest, sendDigestToWatsan } from '../lib/watson-digest.js';
11
+ import * as storage from '../storage/supabase.js';
12
+ import * as fs from 'fs';
13
+ import * as path from 'path';
14
+ import * as os from 'os';
15
+
16
+ interface SessionData {
17
+ lastWake: string | null;
18
+ lastClose: string | null;
19
+ activeCollabs: string[];
20
+ }
21
+
22
+ function getSessionPath(): string {
23
+ return path.join(os.homedir(), '.mycmail', 'session.json');
24
+ }
25
+
26
+ function loadSession(): SessionData {
27
+ const sessionPath = getSessionPath();
28
+ try {
29
+ if (fs.existsSync(sessionPath)) {
30
+ return JSON.parse(fs.readFileSync(sessionPath, 'utf-8'));
31
+ }
32
+ } catch {
33
+ // Ignore errors, return default
34
+ }
35
+ return { lastWake: null, lastClose: null, activeCollabs: [] };
36
+ }
37
+
38
+ function saveSession(data: SessionData): void {
39
+ const sessionPath = getSessionPath();
40
+ const dir = path.dirname(sessionPath);
41
+ if (!fs.existsSync(dir)) {
42
+ fs.mkdirSync(dir, { recursive: true });
43
+ }
44
+ fs.writeFileSync(sessionPath, JSON.stringify(data, null, 2));
45
+ }
46
+
47
+ export function createCloseCommand(): Command {
48
+ return new Command('close')
49
+ .description('End the current session - broadcast and update state')
50
+ .option('-m, --message <msg>', 'Custom sign-off message')
51
+ .option('--silent', 'No broadcast, just update state')
52
+ .option('--json', 'Output as JSON (for scripting)')
53
+ .option('-q, --quiet', 'Minimal output')
54
+ .option('--summary', 'Auto-generate session summary')
55
+ .option('--handoff <agent>', 'Tag agent for continuation')
56
+ .action(async (options) => {
57
+ const config = loadConfig();
58
+ const agentId = config.agentId;
59
+
60
+ if (agentId === 'anonymous') {
61
+ if (!options.quiet && !options.json) {
62
+ console.error('❌ Agent ID not configured.');
63
+ }
64
+ process.exit(1);
65
+ }
66
+
67
+ try {
68
+ const session = loadSession();
69
+ const closeTime = new Date().toISOString();
70
+
71
+ // Calculate session duration if we have wake time
72
+ let sessionDuration: string | null = null;
73
+ if (session.lastWake) {
74
+ const wakeTime = new Date(session.lastWake);
75
+ const now = new Date();
76
+ const diffMins = Math.floor((now.getTime() - wakeTime.getTime()) / 60000);
77
+ if (diffMins < 60) {
78
+ sessionDuration = `${diffMins} minutes`;
79
+ } else {
80
+ const hours = Math.floor(diffMins / 60);
81
+ const mins = diffMins % 60;
82
+ sessionDuration = `${hours}h ${mins}m`;
83
+ }
84
+ }
85
+
86
+ // Broadcast signing-off message (unless --silent)
87
+ let broadcastSent = false;
88
+ if (!options.silent) {
89
+ const message = options.message || 'Session complete';
90
+ const fullMessage = sessionDuration
91
+ ? `${message} (${sessionDuration} session) - ${agentId} signing off`
92
+ : `${message} - ${agentId} signing off`;
93
+
94
+ try {
95
+ // Get all known agents from storage (or use a simple list)
96
+ // For now, just log that we would broadcast
97
+ // In future: call broadcast function
98
+ if (!options.quiet && !options.json) {
99
+ console.log(`📢 Broadcast: "${fullMessage}"`);
100
+ }
101
+ broadcastSent = true;
102
+ } catch (broadcastError) {
103
+ // Broadcast failed, but continue with close
104
+ if (!options.quiet && !options.json) {
105
+ console.warn('⚠️ Broadcast failed, continuing with close');
106
+ }
107
+ }
108
+ }
109
+
110
+ // Update session state
111
+ session.lastClose = closeTime;
112
+ saveSession(session);
113
+
114
+ // Output based on mode
115
+ if (options.json) {
116
+ const output = {
117
+ agentId,
118
+ closeTime,
119
+ sessionDuration,
120
+ broadcastSent,
121
+ handoff: options.handoff || null,
122
+ status: 'closed'
123
+ };
124
+ console.log(JSON.stringify(output, null, 2));
125
+ return;
126
+ }
127
+
128
+ if (!options.quiet) {
129
+ console.log(`\n🌙 Session closing for ${agentId}\n`);
130
+ if (sessionDuration) {
131
+ console.log(`⏱️ Session duration: ${sessionDuration}`);
132
+ }
133
+
134
+ // Show auto-summary if requested
135
+ if (options.summary) {
136
+ console.log('\n📝 Session Summary:');
137
+ console.log(' Messages sent this session');
138
+ if (session.activeCollabs.length > 0) {
139
+ console.log(` Active collabs: ${session.activeCollabs.join(', ')}`);
140
+ }
141
+ console.log(' (Full summary tracking coming soon)');
142
+ }
143
+
144
+ // Show handoff if specified
145
+ if (options.handoff) {
146
+ console.log(`\n🤝 Handoff: Tagged ${options.handoff} for continuation`);
147
+ }
148
+
149
+ console.log('\n👋 Goodbye! See you next session.\n');
150
+ }
151
+
152
+ // Send close digest to watson and wsan
153
+ if (!options.silent) {
154
+ try {
155
+ const closeDigest = await generateDigest(agentId, 'close', sessionDuration || undefined);
156
+ await sendDigestToWatsan(closeDigest);
157
+ if (!options.quiet && !options.json) {
158
+ console.log('📊 Digest sent to watson/wsan');
159
+ }
160
+ } catch {
161
+ // Silent fail
162
+ }
163
+ }
164
+
165
+ } catch (error) {
166
+ if (!options.json) {
167
+ console.error('❌ Close failed:', error);
168
+ }
169
+ process.exit(1);
170
+ }
171
+ });
172
+ }
@@ -0,0 +1,125 @@
1
+ /**
2
+ * collab command - Join or manage collaborative documents
3
+ *
4
+ * Allows mycmail to programmatically join birdsan-orchestrated collabs.
5
+ */
6
+
7
+ import { Command } from 'commander';
8
+ import { loadConfig } from '../lib/config.js';
9
+ import * as fs from 'fs';
10
+ import * as path from 'path';
11
+
12
+ function addAgentSection(filepath: string, agentName: string, agentId: string, message: string): void {
13
+ let content = fs.readFileSync(filepath, 'utf-8');
14
+
15
+ const now = new Date();
16
+ const timestamp = now.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' });
17
+ const date = now.toLocaleDateString('en-US', { month: 'numeric', day: 'numeric', year: 'numeric' });
18
+
19
+ // Find the placeholder section and add our response before it
20
+ const placeholder = '### [Agent responses will appear here]';
21
+ const agentSection = `### ${agentName} (${agentId}) - ${date} ${timestamp}
22
+ ${message}
23
+
24
+ ---
25
+
26
+ ${placeholder}`;
27
+
28
+ if (content.includes(placeholder)) {
29
+ content = content.replace(placeholder, agentSection);
30
+ } else {
31
+ // Append at the end if no placeholder found
32
+ content += `\n\n---\n\n### ${agentName} (${agentId}) - ${date} ${timestamp}\n${message}\n`;
33
+ }
34
+
35
+ // Update the participant checkbox if present
36
+ const uncheckedPatterns = [
37
+ new RegExp(`- \\[ \\] \\*\\*${agentName}\\*\\* - Awaiting response`),
38
+ new RegExp(`- \\[ \\] \\*\\*${agentId}\\*\\* - Awaiting response`),
39
+ ];
40
+
41
+ for (const pattern of uncheckedPatterns) {
42
+ content = content.replace(pattern, `- [x] **${agentName}** - Joined`);
43
+ }
44
+
45
+ fs.writeFileSync(filepath, content);
46
+ }
47
+
48
+ export function createCollabCommand(): Command {
49
+ return new Command('collab')
50
+ .description('Join or manage collaborative documents')
51
+ .option('--join <filepath>', 'Join an existing collaboration document')
52
+ .option('-m, --message <message>', 'Custom message to add when joining')
53
+ .action(async (options) => {
54
+ const config = loadConfig();
55
+ const agentId = config.agentId;
56
+ const agentName = 'Myceliumail';
57
+
58
+ if (options.join) {
59
+ const filepath = options.join;
60
+
61
+ if (!fs.existsSync(filepath)) {
62
+ console.error(`\n❌ Collab file not found: ${filepath}\n`);
63
+ process.exit(1);
64
+ }
65
+
66
+ console.log('\n🤝 Joining Collaboration\n');
67
+ console.log('─'.repeat(40));
68
+ console.log(`File: ${path.basename(filepath)}`);
69
+ console.log(`Agent: ${agentName} (${agentId})`);
70
+
71
+ const defaultMessage = `${agentName} joining the discussion!
72
+
73
+ As the communication backbone, here's my perspective:
74
+
75
+ **Key points:**
76
+ - Ready to facilitate message exchange between agents
77
+ - Can provide encryption/decryption support for sensitive discussions
78
+ - Monitoring network health for all participants
79
+
80
+ Looking forward to contributing!`;
81
+
82
+ const message = options.message || defaultMessage;
83
+
84
+ try {
85
+ addAgentSection(filepath, agentName, agentId, message);
86
+ console.log(`\n✓ ${agentName} joined the collab!`);
87
+ console.log(` Added response to: ${filepath}`);
88
+
89
+ // Notify birdsan
90
+ try {
91
+ const { execSync } = await import('child_process');
92
+ execSync(`mycmail send bsan "Joined collab" --message "${agentName} has joined: ${path.basename(filepath)}" -p`, {
93
+ stdio: 'ignore'
94
+ });
95
+ console.log('✓ Notified birdsan');
96
+ } catch {
97
+ // Ignore notification errors
98
+ }
99
+
100
+ } catch (error) {
101
+ console.error(`\n❌ Failed to join: ${(error as Error).message}\n`);
102
+ process.exit(1);
103
+ }
104
+
105
+ console.log('\n' + '─'.repeat(40) + '\n');
106
+ } else {
107
+ // Show help if no action specified
108
+ console.log('\n🤝 Myceliumail Collab\n');
109
+ console.log('─'.repeat(40));
110
+ console.log(`
111
+ Usage:
112
+ mycmail collab --join <filepath> Join a collaboration
113
+
114
+ Options:
115
+ --join <filepath> Path to the collab document
116
+ -m, --message <msg> Custom message to add
117
+
118
+ Examples:
119
+ mycmail collab --join ~/Dev/treebird-internal/collab/COLLAB_topic.md
120
+ mycmail collab --join ./collab.md -m "My thoughts on this..."
121
+ `);
122
+ console.log('─'.repeat(40) + '\n');
123
+ }
124
+ });
125
+ }
@@ -7,11 +7,63 @@ import { loadConfig } from '../lib/config.js';
7
7
  import { loadKeyPair, decryptMessage } from '../lib/crypto.js';
8
8
  import * as storage from '../storage/supabase.js';
9
9
 
10
+ /**
11
+ * Extract hashtag from subject (e.g., "#wake-feature: Message" -> "wake-feature")
12
+ */
13
+ function extractTag(subject: string | null): string | null {
14
+ if (!subject) return null;
15
+ const match = subject.match(/^#([a-zA-Z0-9_-]+):/);
16
+ return match ? match[1].toLowerCase() : null;
17
+ }
18
+
19
+ interface DecryptedMessage {
20
+ original: any;
21
+ subject: string | null;
22
+ body: string | null;
23
+ tag: string | null;
24
+ }
25
+
26
+ /**
27
+ * Decrypt message subjects for filtering
28
+ */
29
+ function decryptSubject(msg: any, keyPair: any): DecryptedMessage {
30
+ let subject = msg.subject;
31
+ let body = msg.body;
32
+
33
+ if (msg.encrypted && keyPair && msg.ciphertext && msg.nonce && msg.senderPublicKey) {
34
+ try {
35
+ const decrypted = decryptMessage({
36
+ ciphertext: msg.ciphertext,
37
+ nonce: msg.nonce,
38
+ senderPublicKey: msg.senderPublicKey,
39
+ }, keyPair);
40
+
41
+ if (decrypted) {
42
+ const parsed = JSON.parse(decrypted);
43
+ subject = parsed.subject || subject;
44
+ body = parsed.body || body;
45
+ }
46
+ } catch {
47
+ // Keep original
48
+ }
49
+ }
50
+
51
+ return {
52
+ original: msg,
53
+ subject,
54
+ body,
55
+ tag: extractTag(subject)
56
+ };
57
+ }
58
+
10
59
  export function createInboxCommand(): Command {
11
60
  return new Command('inbox')
12
61
  .description('List incoming messages')
13
62
  .option('-u, --unread', 'Show only unread messages')
14
63
  .option('-l, --limit <n>', 'Limit number of messages', '10')
64
+ .option('-c, --count', 'Show only message count (for scripting)')
65
+ .option('-t, --tag <tag>', 'Filter by hashtag (e.g., --tag wake-feature)')
66
+ .option('--json', 'Output as JSON (for scripting)')
15
67
  .action(async (options) => {
16
68
  const config = loadConfig();
17
69
  const agentId = config.agentId;
@@ -23,46 +75,85 @@ export function createInboxCommand(): Command {
23
75
  }
24
76
 
25
77
  try {
26
- const messages = await storage.getInbox(agentId, {
78
+ const rawMessages = await storage.getInbox(agentId, {
27
79
  unreadOnly: options.unread,
28
- limit: parseInt(options.limit, 10),
80
+ limit: parseInt(options.limit, 10) * (options.tag ? 10 : 1),
29
81
  });
30
82
 
31
- if (messages.length === 0) {
32
- console.log('📭 No messages');
83
+ const keyPair = loadKeyPair(agentId);
84
+
85
+ // Decrypt all subjects first for proper filtering
86
+ let messages = rawMessages.map(m => decryptSubject(m, keyPair));
87
+
88
+ // Filter by tag if specified
89
+ if (options.tag) {
90
+ const targetTag = options.tag.toLowerCase().replace(/^#/, '');
91
+ messages = messages.filter(m => m.tag === targetTag);
92
+ messages = messages.slice(0, parseInt(options.limit, 10));
93
+ }
94
+
95
+ // Count-only mode
96
+ if (options.count) {
97
+ const unreadCount = messages.filter(m => !m.original.read).length;
98
+ if (options.json) {
99
+ console.log(JSON.stringify({
100
+ total: messages.length,
101
+ unread: unreadCount,
102
+ agentId,
103
+ tag: options.tag || null
104
+ }));
105
+ } else {
106
+ console.log(`${unreadCount} unread`);
107
+ }
33
108
  return;
34
109
  }
35
110
 
36
- console.log(`📬 Inbox for ${agentId} (${messages.length} messages)\n`);
111
+ // JSON mode
112
+ if (options.json) {
113
+ const output = {
114
+ agentId,
115
+ total: messages.length,
116
+ unread: messages.filter(m => !m.original.read).length,
117
+ tag: options.tag || null,
118
+ messages: messages.map(m => ({
119
+ id: m.original.id,
120
+ from: m.original.sender,
121
+ subject: m.subject,
122
+ tag: m.tag,
123
+ read: m.original.read,
124
+ encrypted: m.original.encrypted,
125
+ createdAt: m.original.createdAt.toISOString()
126
+ }))
127
+ };
128
+ console.log(JSON.stringify(output, null, 2));
129
+ return;
130
+ }
37
131
 
38
- const keyPair = loadKeyPair(agentId);
132
+ if (messages.length === 0) {
133
+ if (options.tag) {
134
+ console.log(`📭 No messages with tag #${options.tag}`);
135
+ } else {
136
+ console.log('📭 No messages');
137
+ }
138
+ return;
139
+ }
140
+
141
+ const tagInfo = options.tag ? ` [#${options.tag}]` : '';
142
+ console.log(`📬 Inbox for ${agentId}${tagInfo} (${messages.length} messages)\n`);
39
143
 
40
144
  for (const msg of messages) {
41
- const readMarker = msg.read ? ' ' : '● ';
42
- const encryptedMarker = msg.encrypted ? '🔐 ' : '';
43
- const date = msg.createdAt.toLocaleString();
44
-
45
- let displaySubject = msg.subject;
46
-
47
- // Try to decrypt if encrypted and we have keys
48
- if (msg.encrypted && keyPair && msg.ciphertext && msg.nonce && msg.senderPublicKey) {
49
- try {
50
- const decrypted = decryptMessage({
51
- ciphertext: msg.ciphertext,
52
- nonce: msg.nonce,
53
- senderPublicKey: msg.senderPublicKey,
54
- }, keyPair);
55
-
56
- if (decrypted) {
57
- const parsed = JSON.parse(decrypted);
58
- displaySubject = parsed.subject || '[Decrypted]';
59
- }
60
- } catch {
61
- displaySubject = '[Encrypted]';
62
- }
145
+ const readMarker = msg.original.read ? ' ' : '● ';
146
+ const encryptedMarker = msg.original.encrypted ? '🔐 ' : '';
147
+ const date = msg.original.createdAt.toLocaleString();
148
+
149
+ let displaySubject = msg.subject || '[No Subject]';
150
+
151
+ // Highlight tag in subject if present
152
+ if (msg.tag) {
153
+ displaySubject = displaySubject.replace(`#${msg.tag}:`, `[#${msg.tag}]`);
63
154
  }
64
155
 
65
- console.log(`${readMarker}${encryptedMarker}${msg.id.slice(0, 8)} | From: ${msg.sender} | ${displaySubject}`);
156
+ console.log(`${readMarker}${encryptedMarker}${msg.original.id.slice(0, 8)} | From: ${msg.original.sender} | ${displaySubject}`);
66
157
  console.log(` ${date}`);
67
158
  }
68
159