metame-cli 1.3.18 โ 1.3.20
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 +77 -0
- package/index.js +19 -4
- package/package.json +1 -1
- package/scripts/daemon.js +347 -123
- package/scripts/distill.js +1 -0
- package/scripts/feishu-adapter.js +76 -11
- package/scripts/schema.js +1 -2
- package/scripts/signal-capture.js +5 -0
package/README.md
CHANGED
|
@@ -54,6 +54,10 @@
|
|
|
54
54
|
* **๐ฅ Emergency Recovery (v1.3.13):** `/doctor` interactive diagnostics with one-tap fix buttons, `/sh` direct shell access from your phone (bypasses Claude entirely โ the lifeline when everything else is broken), automatic config backup before any setting change, `/fix` to restore last known good config. `/model` interactive model switcher with auto-backup.
|
|
55
55
|
* **๐ Browser Automation (v1.3.15):** Native Playwright MCP integration โ auto-registered on first run. Every MetaMe user gets browser control capability out of the box. Combined with Skills, enables workflows like automated podcast publishing, form filling, and web scraping.
|
|
56
56
|
* **๐ Interactive File Browser (v1.3.15):** `/list` shows clickable button cards โ folders expand inline, files download on tap. Folder buttons survive daemon restarts (absolute paths, no expiry). Zero token cost.
|
|
57
|
+
* **๐ Parallel Multi-Agent Chats (v1.3.19):** Assign each agent its own dedicated Feishu/Telegram group. Messages in different groups execute in parallel โ no waiting for each other. Route `chatId โ agent` via `chat_agent_map` in `daemon.yaml`. Create a new group, send `/bind <name>` to register it instantly.
|
|
58
|
+
* **๐ง Config Hot-Reload Fix (v1.3.19):** `allowed_chat_ids` is now read dynamically on every message โ no restart needed after editing `daemon.yaml`. `/fix` config restore now merges current `chatId` settings so manually-added groups are never lost.
|
|
59
|
+
* **๐ก๏ธ Daemon Auto-Restart via LaunchAgent (v1.3.19):** MetaMe's npm daemon is now managed by macOS launchd. Crashes or unexpected exits trigger an automatic restart after 5 seconds.
|
|
60
|
+
* **๐ฅ Operator Permissions & Read-Only Mode (v1.3.19):** Add `operator_ids` to restrict who can execute Claude commands in shared groups. Non-operators can still chat and query (read/search/web only) โ they just can't edit files, run bash, or trigger slash commands. Use `/myid` to discover any user's Feishu open_id.
|
|
57
61
|
|
|
58
62
|
## ๐ Prerequisites
|
|
59
63
|
|
|
@@ -341,6 +345,9 @@ Bot: ๅ้ๅฐๅชไธ่ฝฎ๏ผ
|
|
|
341
345
|
| `/budget` | Today's token usage |
|
|
342
346
|
| `/quiet` | Silence mirror/reflections for 48h |
|
|
343
347
|
| `/reload` | Manually reload daemon.yaml (also auto-reloads on file change) |
|
|
348
|
+
| `/bind <name>` | Register current group as a dedicated agent chat โ opens directory browser to pick working directory |
|
|
349
|
+
| `/chatid` | Show the current group's chat ID |
|
|
350
|
+
| `/myid` | Show your own Feishu sender open_id (for configuring `operator_ids`) |
|
|
344
351
|
|
|
345
352
|
**Heartbeat Tasks:**
|
|
346
353
|
|
|
@@ -395,6 +402,7 @@ Each step runs in the same Claude Code session. Step outputs automatically becom
|
|
|
395
402
|
**Security:**
|
|
396
403
|
|
|
397
404
|
* `allowed_chat_ids` whitelist โ unauthorized users silently ignored (empty = deny all)
|
|
405
|
+
* `operator_ids` โ within an allowed group, restrict command execution to specific users; non-operators get read-only chat mode
|
|
398
406
|
* `dangerously_skip_permissions` enabled by default for mobile (users can't click "allow" on phone โ security relies on the chat ID whitelist)
|
|
399
407
|
* `~/.metame/` directory set to mode 700
|
|
400
408
|
* Bot tokens stored locally, never transmitted
|
|
@@ -446,6 +454,74 @@ projects:
|
|
|
446
454
|
|
|
447
455
|
**Heartbeat task notifications** arrive as colored Feishu cards โ each project's color is distinct, so you can tell at a glance which agent sent the update.
|
|
448
456
|
|
|
457
|
+
### Parallel Multi-Agent Chats & `/bind` Command (v1.3.19)
|
|
458
|
+
|
|
459
|
+
Give each agent its own dedicated group chat โ messages to different groups execute simultaneously without blocking each other.
|
|
460
|
+
|
|
461
|
+
**How it works:**
|
|
462
|
+
|
|
463
|
+
Each group is mapped to a specific agent via `chat_agent_map` in `daemon.yaml`. When a message arrives in a group, the daemon looks up which agent owns that `chatId` and dispatches the Claude call to that agent's working directory โ fully parallel.
|
|
464
|
+
|
|
465
|
+
**Setup โ `/bind` command (recommended):**
|
|
466
|
+
|
|
467
|
+
1. Create a new Feishu or Telegram group and add your bot.
|
|
468
|
+
2. In the group, send `/bind <name>` (e.g., `/bind backend`).
|
|
469
|
+
3. The bot opens a Finder-style directory browser โ tap folders to navigate, tap a folder name to select it as the working directory.
|
|
470
|
+
4. Done. The bot automatically:
|
|
471
|
+
- Adds the group's `chatId` to `allowed_chat_ids`
|
|
472
|
+
- Creates a `chat_agent_map` entry routing this group to the agent
|
|
473
|
+
- Creates a `projects` entry for the agent
|
|
474
|
+
- Sends a welcome card
|
|
475
|
+
|
|
476
|
+
> **No whitelist required:** `/bind` works in any group โ the new group self-registers without needing to be pre-approved in `allowed_chat_ids`.
|
|
477
|
+
|
|
478
|
+
> **Re-binding:** Send `/bind <name>` again in the same group to overwrite the previous configuration.
|
|
479
|
+
|
|
480
|
+
**Manual setup** (`~/.metame/daemon.yaml`):
|
|
481
|
+
|
|
482
|
+
```yaml
|
|
483
|
+
chat_agent_map:
|
|
484
|
+
"oc_abc123": "backend" # chatId โ project key
|
|
485
|
+
"oc_def456": "frontend"
|
|
486
|
+
|
|
487
|
+
projects:
|
|
488
|
+
backend:
|
|
489
|
+
name: "Backend API"
|
|
490
|
+
cwd: "~/projects/api"
|
|
491
|
+
frontend:
|
|
492
|
+
name: "Frontend App"
|
|
493
|
+
cwd: "~/projects/app"
|
|
494
|
+
```
|
|
495
|
+
|
|
496
|
+
**`/chatid` command:**
|
|
497
|
+
|
|
498
|
+
In any authorized group, send `/chatid` and the bot replies with the current group's `chatId`. Useful for manual configuration.
|
|
499
|
+
|
|
500
|
+
| Command | Description |
|
|
501
|
+
|---------|-------------|
|
|
502
|
+
| `/bind <name>` | Register current group as a dedicated agent chat โ opens directory browser to pick working directory |
|
|
503
|
+
| `/chatid` | Show the current group's chat ID |
|
|
504
|
+
| `/myid` | Show your own Feishu sender open_id |
|
|
505
|
+
|
|
506
|
+
**Operator Permissions (`operator_ids`):**
|
|
507
|
+
|
|
508
|
+
In shared groups (e.g., a group with a colleague or tester), you can restrict who can execute Claude commands. Non-operators get a read-only chat mode โ they can ask questions and search, but can't edit files, run bash, or trigger slash commands.
|
|
509
|
+
|
|
510
|
+
```yaml
|
|
511
|
+
feishu:
|
|
512
|
+
operator_ids:
|
|
513
|
+
- "ou_abc123yourid" # Only these users can execute commands
|
|
514
|
+
```
|
|
515
|
+
|
|
516
|
+
Use `/myid` in any Feishu group to get a user's open_id. Then add it to `operator_ids` to grant full access.
|
|
517
|
+
|
|
518
|
+
| User type | Chat & query | Slash commands | Write / Edit / Bash |
|
|
519
|
+
|-----------|:---:|:---:|:---:|
|
|
520
|
+
| Operator | โ
| โ
| โ
|
|
|
521
|
+
| Non-operator | โ
| โ | โ |
|
|
522
|
+
|
|
523
|
+
> If `operator_ids` is empty, all whitelisted users have full access (default behavior).
|
|
524
|
+
|
|
449
525
|
### Provider Relay โ Third-Party Model Support (v1.3.11)
|
|
450
526
|
|
|
451
527
|
MetaMe supports any Anthropic-compatible API relay as a backend. This means you can route Claude Code through a third-party relay that maps `sonnet`/`opus`/`haiku` to any model (GPT-4, DeepSeek, Gemini, etc.) โ MetaMe passes standard model names and the relay handles translation.
|
|
@@ -649,6 +725,7 @@ A: No. Your profile stays local at `~/.claude_profile.yaml`. MetaMe simply passe
|
|
|
649
725
|
|
|
650
726
|
| Version | Highlights |
|
|
651
727
|
|---------|------------|
|
|
728
|
+
| **v1.3.19** | **Parallel multi-agent group chats** โ `chat_agent_map` routes chatId โ agent for true parallel execution; `/bind` command for one-tap group registration with Finder-style directory browser; `/chatid` to look up group ID; `allowed_chat_ids` hot-reload fix (read per message, no restart); `/fix` now merges current chatId config; daemon auto-restart via macOS LaunchAgent (5-second recovery); **operator_ids** permission layer โ non-operators get read-only chat mode (query/search, no write/execute); `/myid` command to retrieve Feishu open_id |
|
|
652
729
|
| **v1.3.18** | **Multi-agent project isolation** โ `projects` in `daemon.yaml` with per-project heartbeat tasks, Feishu colored cards per project, `/agent` picker button, nickname routing (say agent name to switch instantly), reply-to-message session restoration, fix `~` expansion in project cwd |
|
|
653
730
|
| **v1.3.17** | **Windows support** (WSL one-command installer), `install-systemd` for Linux/WSL daemon auto-start. Fix onboarding (Genesis interview was never injected, CLAUDE.md accumulated across runs). Marker-based cleanup, unified protocols, `--append-system-prompt` guarantees interview activation, Feishu auto-fetch chat ID, full mobile permissions, fix `/publish` false-success, auto-restart daemon on script update |
|
|
654
731
|
| **v1.3.16** | Git-based `/undo` (auto-checkpoint before each turn, `git reset --hard` rollback), `/nosleep` toggle (macOS caffeinate), custom provider model passthrough (`/model` accepts any name for non-anthropic providers), auto-fallback to anthropic/opus on provider failure, message queue works on Telegram (fire-and-forget poll loop), lazy background distill |
|
package/index.js
CHANGED
|
@@ -203,6 +203,21 @@ function spawnDistillBackground() {
|
|
|
203
203
|
} catch { /* stale lock, proceed */ }
|
|
204
204
|
}
|
|
205
205
|
|
|
206
|
+
// 4-hour cooldown: check last distill timestamp from profile
|
|
207
|
+
const cooldownMs = 4 * 60 * 60 * 1000;
|
|
208
|
+
try {
|
|
209
|
+
const profilePath = path.join(process.env.HOME || '', '.claude_profile.yaml');
|
|
210
|
+
if (fs.existsSync(profilePath)) {
|
|
211
|
+
const yaml = require('js-yaml');
|
|
212
|
+
const profile = yaml.load(fs.readFileSync(profilePath, 'utf8'));
|
|
213
|
+
const distillLog = profile && profile.evolution && profile.evolution.auto_distill;
|
|
214
|
+
if (Array.isArray(distillLog) && distillLog.length > 0) {
|
|
215
|
+
const lastTs = new Date(distillLog[distillLog.length - 1].ts).getTime();
|
|
216
|
+
if (Date.now() - lastTs < cooldownMs) return;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
} catch { /* non-fatal, proceed */ }
|
|
220
|
+
|
|
206
221
|
const hasSignals = shouldDistill();
|
|
207
222
|
const bootstrap = needsBootstrap();
|
|
208
223
|
|
|
@@ -305,7 +320,7 @@ runExpiryCleanup();
|
|
|
305
320
|
if (!fs.existsSync(BRAIN_FILE)) {
|
|
306
321
|
const initialProfile = `identity:
|
|
307
322
|
role: Unknown
|
|
308
|
-
|
|
323
|
+
locale: null
|
|
309
324
|
status:
|
|
310
325
|
focus: Initializing
|
|
311
326
|
`;
|
|
@@ -367,12 +382,12 @@ You are entering **Calibration Mode**. You are not a chatbot; you are a Psycholo
|
|
|
367
382
|
|
|
368
383
|
5. **Shadows (Hidden Fears):** What are you avoiding? What pattern do you keep repeating? What keeps you up at night?
|
|
369
384
|
|
|
370
|
-
6. **Identity (
|
|
385
|
+
6. **Identity (Role + Locale):** Based on everything learned, propose a role summary and confirm their preferred language (locale). Ask if it resonates.
|
|
371
386
|
|
|
372
387
|
**TERMINATION:**
|
|
373
388
|
- After 5-7 exchanges, synthesize everything into \`~/.claude_profile.yaml\`.
|
|
374
389
|
- **LOCK** Core Values with \`# [LOCKED]\`.
|
|
375
|
-
- Announce: "Link Established.
|
|
390
|
+
- Announce: "Link Established. Profile calibrated."
|
|
376
391
|
- Then proceed to **Phase 2** below.
|
|
377
392
|
|
|
378
393
|
**3. SETUP WIZARD (Phase 2 โ Optional):**
|
|
@@ -440,7 +455,7 @@ let isKnownUser = false;
|
|
|
440
455
|
try {
|
|
441
456
|
if (fs.existsSync(BRAIN_FILE)) {
|
|
442
457
|
const doc = yaml.load(fs.readFileSync(BRAIN_FILE, 'utf8')) || {};
|
|
443
|
-
if (doc.identity && doc.identity.
|
|
458
|
+
if (doc.identity && doc.identity.locale && doc.identity.locale !== 'null') {
|
|
444
459
|
isKnownUser = true;
|
|
445
460
|
}
|
|
446
461
|
}
|
package/package.json
CHANGED
package/scripts/daemon.js
CHANGED
|
@@ -126,12 +126,34 @@ function backupConfig() {
|
|
|
126
126
|
|
|
127
127
|
function restoreConfig() {
|
|
128
128
|
const bak = CONFIG_FILE + '.bak';
|
|
129
|
-
if (fs.existsSync(bak))
|
|
129
|
+
if (!fs.existsSync(bak)) return false;
|
|
130
|
+
try {
|
|
131
|
+
const bakCfg = yaml.load(fs.readFileSync(bak, 'utf8')) || {};
|
|
132
|
+
// Preserve security-critical fields from current config (chat IDs, agent map)
|
|
133
|
+
// so a /fix never loses manually-added channels
|
|
134
|
+
let curCfg = {};
|
|
135
|
+
try { curCfg = yaml.load(fs.readFileSync(CONFIG_FILE, 'utf8')) || {}; } catch {}
|
|
136
|
+
for (const adapter of ['feishu', 'telegram']) {
|
|
137
|
+
if (curCfg[adapter] && bakCfg[adapter]) {
|
|
138
|
+
const curIds = curCfg[adapter].allowed_chat_ids || [];
|
|
139
|
+
const bakIds = bakCfg[adapter].allowed_chat_ids || [];
|
|
140
|
+
// Union of both lists
|
|
141
|
+
const merged = [...new Set([...bakIds, ...curIds])];
|
|
142
|
+
bakCfg[adapter].allowed_chat_ids = merged;
|
|
143
|
+
// Merge chat_agent_map (current takes precedence)
|
|
144
|
+
bakCfg[adapter].chat_agent_map = Object.assign(
|
|
145
|
+
{}, bakCfg[adapter].chat_agent_map || {}, curCfg[adapter].chat_agent_map || {}
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
fs.writeFileSync(CONFIG_FILE, yaml.dump(bakCfg, { lineWidth: -1 }), 'utf8');
|
|
150
|
+
config = loadConfig();
|
|
151
|
+
return true;
|
|
152
|
+
} catch {
|
|
130
153
|
fs.copyFileSync(bak, CONFIG_FILE);
|
|
131
154
|
config = loadConfig();
|
|
132
155
|
return true;
|
|
133
156
|
}
|
|
134
|
-
return false;
|
|
135
157
|
}
|
|
136
158
|
|
|
137
159
|
let _cachedState = null;
|
|
@@ -554,7 +576,7 @@ async function startTelegramBridge(config, executeTaskByName) {
|
|
|
554
576
|
|
|
555
577
|
const { createBot } = require(path.join(__dirname, 'telegram-adapter.js'));
|
|
556
578
|
const bot = createBot(config.telegram.bot_token);
|
|
557
|
-
|
|
579
|
+
// allowedIds read dynamically per-message to support hot-reload of daemon.yaml
|
|
558
580
|
|
|
559
581
|
// Verify bot
|
|
560
582
|
try {
|
|
@@ -581,6 +603,7 @@ async function startTelegramBridge(config, executeTaskByName) {
|
|
|
581
603
|
const chatId = cb.message && cb.message.chat.id;
|
|
582
604
|
bot.answerCallback(cb.id).catch(() => {});
|
|
583
605
|
if (chatId && cb.data) {
|
|
606
|
+
const allowedIds = (loadConfig().telegram && loadConfig().telegram.allowed_chat_ids) || [];
|
|
584
607
|
if (!allowedIds.includes(chatId)) continue;
|
|
585
608
|
// Fire-and-forget: don't block poll loop (enables message queue)
|
|
586
609
|
handleCommand(bot, chatId, cb.data, config, executeTaskByName).catch(e => {
|
|
@@ -595,8 +618,11 @@ async function startTelegramBridge(config, executeTaskByName) {
|
|
|
595
618
|
const msg = update.message;
|
|
596
619
|
const chatId = msg.chat.id;
|
|
597
620
|
|
|
598
|
-
// Security: check whitelist (empty = deny all)
|
|
599
|
-
|
|
621
|
+
// Security: check whitelist (empty = deny all) โ read live config to support hot-reload
|
|
622
|
+
// Exception: /bind is allowed from any chat so users can self-register new groups
|
|
623
|
+
const allowedIds = (loadConfig().telegram && loadConfig().telegram.allowed_chat_ids) || [];
|
|
624
|
+
const isBindCmd = msg.text && msg.text.trim().startsWith('/bind');
|
|
625
|
+
if (!allowedIds.includes(chatId) && !isBindCmd) {
|
|
600
626
|
log('WARN', `Rejected message from unauthorized chat: ${chatId}`);
|
|
601
627
|
continue;
|
|
602
628
|
}
|
|
@@ -670,9 +696,15 @@ async function startTelegramBridge(config, executeTaskByName) {
|
|
|
670
696
|
};
|
|
671
697
|
}
|
|
672
698
|
|
|
699
|
+
// โโ Timing constants โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
700
|
+
const CLAUDE_COOLDOWN_MS = 10000; // 10s between Claude calls per chat
|
|
701
|
+
const STATUS_THROTTLE_MS = 3000; // Min 3s between streaming status updates
|
|
702
|
+
const FALLBACK_THROTTLE_MS = 8000; // 8s between fallback status updates
|
|
703
|
+
const DEDUP_TTL_MS = 60000; // Feishu message dedup window (60s)
|
|
704
|
+
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
705
|
+
|
|
673
706
|
// Rate limiter for /ask and /run โ prevents rapid-fire Claude calls
|
|
674
707
|
const _lastClaudeCall = {};
|
|
675
|
-
const CLAUDE_COOLDOWN_MS = 10000; // 10s between Claude calls per chat
|
|
676
708
|
|
|
677
709
|
function checkCooldown(chatId) {
|
|
678
710
|
const now = Date.now();
|
|
@@ -688,63 +720,126 @@ function checkCooldown(chatId) {
|
|
|
688
720
|
// Path shortener โ imported from ./utils
|
|
689
721
|
const { shortenPath, expandPath } = createPathMap();
|
|
690
722
|
|
|
723
|
+
/**
|
|
724
|
+
* Normalize a directory path: expand shortcuts and resolve ~
|
|
725
|
+
*/
|
|
726
|
+
function normalizeCwd(p) {
|
|
727
|
+
return expandPath(p).replace(/^~/, HOME);
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
/**
|
|
731
|
+
* Parse [[FILE:...]] markers from Claude output.
|
|
732
|
+
* Returns { markedFiles, cleanOutput }
|
|
733
|
+
*/
|
|
734
|
+
function parseFileMarkers(output) {
|
|
735
|
+
const markers = output.match(/\[\[FILE:([^\]]+)\]\]/g) || [];
|
|
736
|
+
const markedFiles = markers.map(m => m.match(/\[\[FILE:([^\]]+)\]\]/)[1].trim());
|
|
737
|
+
const cleanOutput = output.replace(/\s*\[\[FILE:[^\]]+\]\]/g, '').trim();
|
|
738
|
+
return { markedFiles, cleanOutput };
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
/**
|
|
742
|
+
* Merge explicit [[FILE:...]] paths with auto-detected content files.
|
|
743
|
+
* Returns a Set of unique file paths.
|
|
744
|
+
*/
|
|
745
|
+
function mergeFileCollections(markedFiles, sourceFiles) {
|
|
746
|
+
const result = new Set(markedFiles);
|
|
747
|
+
if (sourceFiles && sourceFiles.length > 0) {
|
|
748
|
+
for (const f of sourceFiles) { if (isContentFile(f)) result.add(f); }
|
|
749
|
+
}
|
|
750
|
+
return result;
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
/**
|
|
754
|
+
* Send file download buttons for a set of file paths.
|
|
755
|
+
*/
|
|
756
|
+
async function sendFileButtons(bot, chatId, files) {
|
|
757
|
+
if (!bot.sendButtons || files.size === 0) return;
|
|
758
|
+
const validFiles = [...files].filter(f => fs.existsSync(f));
|
|
759
|
+
if (validFiles.length === 0) return;
|
|
760
|
+
const buttons = validFiles.map(filePath => {
|
|
761
|
+
const shortId = cacheFile(filePath);
|
|
762
|
+
return [{ text: `๐ ${path.basename(filePath)}`, callback_data: `/file ${shortId}` }];
|
|
763
|
+
});
|
|
764
|
+
await bot.sendButtons(chatId, '๐ ๆไปถ:', buttons);
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
/**
|
|
768
|
+
* Attach chatId to the most recent session in projCwd, or create a new one.
|
|
769
|
+
*/
|
|
770
|
+
function attachOrCreateSession(chatId, projCwd, name) {
|
|
771
|
+
const state = loadState();
|
|
772
|
+
const recent = listRecentSessions(1, projCwd);
|
|
773
|
+
if (recent.length > 0 && recent[0].sessionId) {
|
|
774
|
+
state.sessions[chatId] = { id: recent[0].sessionId, cwd: projCwd, started: true };
|
|
775
|
+
} else {
|
|
776
|
+
const newSess = createSession(chatId, projCwd, name || '');
|
|
777
|
+
state.sessions[chatId] = { id: newSess.id, cwd: projCwd, started: false };
|
|
778
|
+
}
|
|
779
|
+
saveState(state);
|
|
780
|
+
}
|
|
781
|
+
|
|
691
782
|
/**
|
|
692
783
|
* Send directory picker: recent projects + Browse button
|
|
693
784
|
* @param {string} mode - 'new' or 'cd' (determines callback command)
|
|
694
785
|
*/
|
|
695
786
|
async function sendDirPicker(bot, chatId, mode, title) {
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
if (bot.sendButtons) {
|
|
699
|
-
const buttons = dirs.map(d => [{ text: d.label, callback_data: `${cmd} ${shortenPath(d.path)}` }]);
|
|
700
|
-
buttons.push([{ text: 'Browse...', callback_data: `/browse ${mode} ${shortenPath(HOME)}` }]);
|
|
701
|
-
await bot.sendButtons(chatId, title, buttons);
|
|
702
|
-
} else {
|
|
703
|
-
let msg = `${title}\n`;
|
|
704
|
-
dirs.forEach((d, i) => { msg += `${i + 1}. ${d.label}\n ${cmd} ${d.path}\n`; });
|
|
705
|
-
msg += `\nOr type: ${cmd} /full/path`;
|
|
706
|
-
await bot.sendMessage(chatId, msg);
|
|
707
|
-
}
|
|
787
|
+
// Always open the file browser starting from HOME โ Finder-style navigation
|
|
788
|
+
await sendBrowse(bot, chatId, mode, HOME, title);
|
|
708
789
|
}
|
|
709
790
|
|
|
710
791
|
/**
|
|
711
|
-
* Send directory browser:
|
|
792
|
+
* Send directory browser: Finder-style navigation
|
|
793
|
+
* - Clicking a subdir ALWAYS navigates into it (never immediate select)
|
|
794
|
+
* - "โ ้ๆฉๆญค็ฎๅฝ" button at top confirms the current dir
|
|
795
|
+
* - Shows up to 12 subdirs per page with pagination
|
|
712
796
|
*/
|
|
713
|
-
async function sendBrowse(bot, chatId, mode, dirPath) {
|
|
714
|
-
const cmd = mode === 'new' ? '/new' : '/cd';
|
|
797
|
+
async function sendBrowse(bot, chatId, mode, dirPath, title, page = 0) {
|
|
798
|
+
const cmd = mode === 'new' ? '/new' : mode === 'bind' ? '/bind-dir' : '/cd';
|
|
799
|
+
const PAGE_SIZE = 10;
|
|
715
800
|
try {
|
|
716
801
|
const entries = fs.readdirSync(dirPath, { withFileTypes: true });
|
|
717
802
|
const subdirs = entries
|
|
718
803
|
.filter(e => e.isDirectory() && !e.name.startsWith('.'))
|
|
719
804
|
.map(e => e.name)
|
|
720
|
-
.sort()
|
|
721
|
-
|
|
805
|
+
.sort();
|
|
806
|
+
|
|
807
|
+
const totalPages = Math.ceil(subdirs.length / PAGE_SIZE);
|
|
808
|
+
const pageSubdirs = subdirs.slice(page * PAGE_SIZE, (page + 1) * PAGE_SIZE);
|
|
809
|
+
const parent = path.dirname(dirPath);
|
|
810
|
+
const displayPath = dirPath.replace(HOME, '~');
|
|
722
811
|
|
|
723
812
|
if (bot.sendButtons) {
|
|
724
813
|
const buttons = [];
|
|
725
|
-
//
|
|
726
|
-
buttons.push([{ text:
|
|
727
|
-
// Subdirectories
|
|
728
|
-
for (const name of
|
|
814
|
+
// โ Confirm current dir
|
|
815
|
+
buttons.push([{ text: `โ ้ๆฉใ${displayPath}ใ`, callback_data: `${cmd} ${shortenPath(dirPath)}` }]);
|
|
816
|
+
// Subdirectories โ click = navigate in
|
|
817
|
+
for (const name of pageSubdirs) {
|
|
729
818
|
const full = path.join(dirPath, name);
|
|
730
|
-
buttons.push([{ text:
|
|
819
|
+
buttons.push([{ text: `๐ ${name}`, callback_data: `/browse ${mode} ${shortenPath(full)}` }]);
|
|
731
820
|
}
|
|
732
|
-
//
|
|
733
|
-
const
|
|
821
|
+
// Pagination
|
|
822
|
+
const nav = [];
|
|
823
|
+
if (page > 0) nav.push({ text: 'โ ไธ้กต', callback_data: `/browse ${mode} ${shortenPath(dirPath)} ${page - 1}` });
|
|
824
|
+
if (page < totalPages - 1) nav.push({ text: 'ไธ้กต โ', callback_data: `/browse ${mode} ${shortenPath(dirPath)} ${page + 1}` });
|
|
825
|
+
if (nav.length) buttons.push(nav);
|
|
826
|
+
// Parent dir
|
|
734
827
|
if (parent !== dirPath) {
|
|
735
|
-
buttons.push([{ text: '
|
|
828
|
+
buttons.push([{ text: 'โฌ ไธ็บง็ฎๅฝ', callback_data: `/browse ${mode} ${shortenPath(parent)}` }]);
|
|
736
829
|
}
|
|
737
|
-
|
|
830
|
+
const header = title ? `${title}\n๐ ${displayPath}` : `๐ ${displayPath}`;
|
|
831
|
+
await bot.sendButtons(chatId, header, buttons);
|
|
738
832
|
} else {
|
|
739
|
-
let msg =
|
|
740
|
-
|
|
741
|
-
msg += `${i + 1}. ${name}/\n /browse ${mode} ${path.join(dirPath, name)}\n`;
|
|
833
|
+
let msg = `๐ ${displayPath}\n\n`;
|
|
834
|
+
pageSubdirs.forEach((name, i) => {
|
|
835
|
+
msg += `${page * PAGE_SIZE + i + 1}. ${name}/\n /browse ${mode} ${path.join(dirPath, name)}\n`;
|
|
742
836
|
});
|
|
743
|
-
msg += `\
|
|
837
|
+
msg += `\nโ ้ๆฉๆญค็ฎๅฝ: ${cmd} ${dirPath}`;
|
|
838
|
+
if (parent !== dirPath) msg += `\nโฌ ไธ็บง: /browse ${mode} ${parent}`;
|
|
744
839
|
await bot.sendMessage(chatId, msg);
|
|
745
840
|
}
|
|
746
841
|
} catch (e) {
|
|
747
|
-
await bot.sendMessage(chatId,
|
|
842
|
+
await bot.sendMessage(chatId, `ๆ ๆณ่ฏปๅ็ฎๅฝ: ${dirPath}`);
|
|
748
843
|
}
|
|
749
844
|
}
|
|
750
845
|
|
|
@@ -851,16 +946,132 @@ async function sendDirListing(bot, chatId, baseDir, arg) {
|
|
|
851
946
|
/**
|
|
852
947
|
* Unified command handler โ shared by Telegram & Feishu
|
|
853
948
|
*/
|
|
854
|
-
|
|
949
|
+
|
|
950
|
+
async function doBindAgent(bot, chatId, agentName, agentCwd) {
|
|
951
|
+
// /bind sets the session context (cwd, CLAUDE.md, project configs) for this chat.
|
|
952
|
+
// The agent can still read/write any path on the machine โ bind only defines
|
|
953
|
+
// which project directory Claude Code uses as its working directory.
|
|
954
|
+
// Calling /bind again overwrites the previous binding (rebind is always allowed).
|
|
955
|
+
try {
|
|
956
|
+
const cfg = loadConfig();
|
|
957
|
+
const isTg = typeof chatId === 'number';
|
|
958
|
+
const ak = isTg ? 'telegram' : 'feishu';
|
|
959
|
+
if (!cfg[ak]) cfg[ak] = {};
|
|
960
|
+
if (!cfg[ak].allowed_chat_ids) cfg[ak].allowed_chat_ids = [];
|
|
961
|
+
if (!cfg[ak].chat_agent_map) cfg[ak].chat_agent_map = {};
|
|
962
|
+
const idVal = isTg ? chatId : String(chatId);
|
|
963
|
+
if (!cfg[ak].allowed_chat_ids.includes(idVal)) cfg[ak].allowed_chat_ids.push(idVal);
|
|
964
|
+
const projectKey = agentName.replace(/[^a-zA-Z0-9_]/g, '_').toLowerCase() || String(chatId);
|
|
965
|
+
cfg[ak].chat_agent_map[String(chatId)] = projectKey;
|
|
966
|
+
if (!cfg.projects) cfg.projects = {};
|
|
967
|
+
const isNew = !cfg.projects[projectKey];
|
|
968
|
+
if (isNew) {
|
|
969
|
+
cfg.projects[projectKey] = { name: agentName, cwd: agentCwd, nicknames: [agentName] };
|
|
970
|
+
} else {
|
|
971
|
+
cfg.projects[projectKey].name = agentName;
|
|
972
|
+
cfg.projects[projectKey].cwd = agentCwd;
|
|
973
|
+
}
|
|
974
|
+
fs.writeFileSync(CONFIG_FILE, yaml.dump(cfg, { lineWidth: -1 }), 'utf8');
|
|
975
|
+
backupConfig();
|
|
976
|
+
|
|
977
|
+
const proj = cfg.projects[projectKey];
|
|
978
|
+
const icon = proj.icon || '๐ค';
|
|
979
|
+
const color = proj.color || 'blue';
|
|
980
|
+
const action = isNew ? '็ปๅฎๆๅ' : '้ๆฐ็ปๅฎ';
|
|
981
|
+
const displayCwd = agentCwd.replace(HOME, '~');
|
|
982
|
+
if (bot.sendCard) {
|
|
983
|
+
await bot.sendCard(chatId, {
|
|
984
|
+
title: `${icon} ${agentName} โ ${action}`,
|
|
985
|
+
body: `**ๅทฅไฝ็ฎๅฝ**\n${displayCwd}\n\n็ดๆฅๅๆถๆฏๅณๅฏๅผๅงๅฏน่ฏ๏ผๆ ้ @bot`,
|
|
986
|
+
color,
|
|
987
|
+
});
|
|
988
|
+
} else {
|
|
989
|
+
await bot.sendMessage(chatId, `${icon} ${agentName} ${action}\n็ฎๅฝ: ${displayCwd}`);
|
|
990
|
+
}
|
|
991
|
+
} catch (e) {
|
|
992
|
+
await bot.sendMessage(chatId, `โ ็ปๅฎๅคฑ่ดฅ: ${e.message}`);
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
async function handleCommand(bot, chatId, text, config, executeTaskByName, senderId = null, readOnly = false) {
|
|
855
997
|
const state = loadState();
|
|
856
998
|
|
|
999
|
+
// --- /chatid: reply with current chatId ---
|
|
1000
|
+
if (text === '/chatid') {
|
|
1001
|
+
await bot.sendMessage(chatId, `Chat ID: \`${chatId}\``);
|
|
1002
|
+
return;
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
// --- /myid: reply with sender's user open_id (for configuring operator_ids) ---
|
|
1006
|
+
if (text === '/myid') {
|
|
1007
|
+
await bot.sendMessage(chatId, senderId ? `Your ID: \`${senderId}\`` : 'ID not available (Telegram not supported)');
|
|
1008
|
+
return;
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
// --- /bind <name> [cwd]: register this chat as a dedicated agent channel ---
|
|
1012
|
+
// With cwd: /bind ๅฐ็พ ~/ โ bind immediately
|
|
1013
|
+
// Without cwd: /bind ๆๆ โ show directory picker
|
|
1014
|
+
if (text.startsWith('/bind ') || text === '/bind') {
|
|
1015
|
+
const args = text.slice(5).trim();
|
|
1016
|
+
const parts = args.split(/\s+/);
|
|
1017
|
+
const agentName = parts[0];
|
|
1018
|
+
const agentCwd = parts.slice(1).join(' ');
|
|
1019
|
+
|
|
1020
|
+
if (!agentName) {
|
|
1021
|
+
await bot.sendMessage(chatId, '็จๆณ: /bind <ๅ็งฐ> [ๅทฅไฝ็ฎๅฝ]\nไพ: /bind ๅฐ็พ ~/\nๆ: /bind ๆๆ (ๅผนๅบ็ฎๅฝ้ๆฉ)');
|
|
1022
|
+
return;
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
if (!agentCwd) {
|
|
1026
|
+
// No cwd given โ show directory picker
|
|
1027
|
+
pendingBinds.set(String(chatId), agentName);
|
|
1028
|
+
await sendDirPicker(bot, chatId, 'bind', `ไธบใ${agentName}ใ้ๆฉๅทฅไฝ็ฎๅฝ:`);
|
|
1029
|
+
return;
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
await doBindAgent(bot, chatId, agentName, agentCwd);
|
|
1033
|
+
return;
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
// --- /bind-dir <path>: called by directory picker to complete a pending bind ---
|
|
1037
|
+
if (text.startsWith('/bind-dir ')) {
|
|
1038
|
+
const dirPath = expandPath(text.slice(10).trim());
|
|
1039
|
+
const agentName = pendingBinds.get(String(chatId));
|
|
1040
|
+
if (!agentName) {
|
|
1041
|
+
await bot.sendMessage(chatId, 'โ ๆฒกๆๅพ
ๅฎๆ็ /bind๏ผ่ฏท้ๆฐๅ้ /bind <ๅ็งฐ>');
|
|
1042
|
+
return;
|
|
1043
|
+
}
|
|
1044
|
+
pendingBinds.delete(String(chatId));
|
|
1045
|
+
await doBindAgent(bot, chatId, agentName, dirPath);
|
|
1046
|
+
return;
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
// --- chat_agent_map: auto-switch agent based on dedicated chatId ---
|
|
1050
|
+
// Configure in daemon.yaml: feishu.chat_agent_map or telegram.chat_agent_map
|
|
1051
|
+
// e.g. chat_agent_map: { "oc_xxx": "personal", "oc_yyy": "metame" }
|
|
1052
|
+
const chatAgentMap = (config.feishu && config.feishu.chat_agent_map) ||
|
|
1053
|
+
(config.telegram && config.telegram.chat_agent_map) || {};
|
|
1054
|
+
const mappedKey = chatAgentMap[String(chatId)];
|
|
1055
|
+
if (mappedKey && config.projects && config.projects[mappedKey]) {
|
|
1056
|
+
const proj = config.projects[mappedKey];
|
|
1057
|
+
const projCwd = normalizeCwd(proj.cwd);
|
|
1058
|
+
const cur = loadState().sessions?.[chatId];
|
|
1059
|
+
if (!cur || cur.cwd !== projCwd) {
|
|
1060
|
+
attachOrCreateSession(chatId, projCwd, proj.name || mappedKey);
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
|
|
857
1064
|
// --- Browse handler (directory navigation) ---
|
|
858
1065
|
if (text.startsWith('/browse ')) {
|
|
859
1066
|
const parts = text.slice(8).trim().split(' ');
|
|
860
|
-
const mode = parts[0]; // 'new' or '
|
|
861
|
-
|
|
1067
|
+
const mode = parts[0]; // 'new', 'cd', or 'bind'
|
|
1068
|
+
// Last token may be a page number
|
|
1069
|
+
const lastPart = parts[parts.length - 1];
|
|
1070
|
+
const page = /^\d+$/.test(lastPart) ? parseInt(lastPart, 10) : 0;
|
|
1071
|
+
const pathParts = /^\d+$/.test(lastPart) ? parts.slice(1, -1) : parts.slice(1);
|
|
1072
|
+
const dirPath = expandPath(pathParts.join(' '));
|
|
862
1073
|
if (mode && dirPath && fs.existsSync(dirPath)) {
|
|
863
|
-
await sendBrowse(bot, chatId, mode, dirPath);
|
|
1074
|
+
await sendBrowse(bot, chatId, mode, dirPath, null, page);
|
|
864
1075
|
} else if (/^p\d+$/.test(dirPath)) {
|
|
865
1076
|
await bot.sendMessage(chatId, 'โ ๏ธ Button expired. Pick again:');
|
|
866
1077
|
await sendDirPicker(bot, chatId, mode || 'cd', 'Switch workdir:');
|
|
@@ -875,6 +1086,19 @@ async function handleCommand(bot, chatId, text, config, executeTaskByName) {
|
|
|
875
1086
|
if (text === '/new' || text.startsWith('/new ')) {
|
|
876
1087
|
const arg = text.slice(4).trim();
|
|
877
1088
|
if (!arg) {
|
|
1089
|
+
// In a dedicated agent group, use the agent's bound cwd directly
|
|
1090
|
+
const newCfg = loadConfig();
|
|
1091
|
+
const agentMap = (newCfg.feishu && newCfg.feishu.chat_agent_map) ||
|
|
1092
|
+
(newCfg.telegram && newCfg.telegram.chat_agent_map) || {};
|
|
1093
|
+
const boundKey = agentMap[String(chatId)];
|
|
1094
|
+
const boundProj = boundKey && newCfg.projects && newCfg.projects[boundKey];
|
|
1095
|
+
if (boundProj && boundProj.cwd) {
|
|
1096
|
+
const boundCwd = normalizeCwd(boundProj.cwd);
|
|
1097
|
+
const session = createSession(chatId, boundCwd, '');
|
|
1098
|
+
await bot.sendMessage(chatId, `โ
ๆฐไผ่ฏๅทฒๅๅปบ\nWorkdir: ${session.cwd}`);
|
|
1099
|
+
return;
|
|
1100
|
+
}
|
|
1101
|
+
// Non-dedicated group: show directory picker
|
|
878
1102
|
await sendDirPicker(bot, chatId, 'new', 'Pick a workdir:');
|
|
879
1103
|
return;
|
|
880
1104
|
}
|
|
@@ -1128,7 +1352,7 @@ async function handleCommand(bot, chatId, text, config, executeTaskByName) {
|
|
|
1128
1352
|
const currentSession = getSession(chatId);
|
|
1129
1353
|
const currentCwd = currentSession?.cwd ? path.resolve(expandPath(currentSession.cwd)) : null;
|
|
1130
1354
|
const buttons = entries.map(([key, p]) => {
|
|
1131
|
-
const projCwd =
|
|
1355
|
+
const projCwd = normalizeCwd(p.cwd);
|
|
1132
1356
|
const active = currentCwd && path.resolve(projCwd) === currentCwd ? ' โ' : '';
|
|
1133
1357
|
return [{ text: `${p.icon || '๐ค'} ${p.name || key}${active}`, callback_data: `/cd ${projCwd}` }];
|
|
1134
1358
|
});
|
|
@@ -1940,7 +2164,7 @@ async function handleCommand(bot, chatId, text, config, executeTaskByName) {
|
|
|
1940
2164
|
q.messages.push(text);
|
|
1941
2165
|
// Only notify once (first message), subsequent ones silently queue
|
|
1942
2166
|
if (isFirst) {
|
|
1943
|
-
await bot.sendMessage(chatId, '๐
|
|
2167
|
+
await bot.sendMessage(chatId, '๐ ๆถๅฐ๏ผ็จๅไธ่ตทๅค็');
|
|
1944
2168
|
}
|
|
1945
2169
|
// Interrupt the running Claude process
|
|
1946
2170
|
const proc = activeProcesses.get(chatId);
|
|
@@ -1972,16 +2196,8 @@ async function handleCommand(bot, chatId, text, config, executeTaskByName) {
|
|
|
1972
2196
|
const quickAgent = routeAgent(text, config);
|
|
1973
2197
|
if (quickAgent && !quickAgent.rest) {
|
|
1974
2198
|
const { key, proj } = quickAgent;
|
|
1975
|
-
const projCwd =
|
|
1976
|
-
|
|
1977
|
-
const recentInDir = listRecentSessions(1, projCwd);
|
|
1978
|
-
if (recentInDir.length > 0 && recentInDir[0].sessionId) {
|
|
1979
|
-
st.sessions[chatId] = { id: recentInDir[0].sessionId, cwd: projCwd, started: true };
|
|
1980
|
-
} else {
|
|
1981
|
-
const newSess = createSession(chatId, projCwd, proj.name || key);
|
|
1982
|
-
st.sessions[chatId] = { id: newSess.id, cwd: projCwd, started: false };
|
|
1983
|
-
}
|
|
1984
|
-
saveState(st);
|
|
2199
|
+
const projCwd = normalizeCwd(proj.cwd);
|
|
2200
|
+
attachOrCreateSession(chatId, projCwd, proj.name || key);
|
|
1985
2201
|
log('INFO', `Agent switch via nickname: ${key} (${projCwd})`);
|
|
1986
2202
|
await bot.sendMessage(chatId, `${proj.icon || '๐ค'} ${proj.name || key} ๅจ็บฟ`);
|
|
1987
2203
|
return;
|
|
@@ -2364,6 +2580,9 @@ const CONTENT_EXTENSIONS = new Set([
|
|
|
2364
2580
|
// Active Claude processes per chat (for /stop)
|
|
2365
2581
|
const activeProcesses = new Map(); // chatId -> { child, aborted }
|
|
2366
2582
|
|
|
2583
|
+
// Pending /bind flows: waiting for user to pick a directory
|
|
2584
|
+
const pendingBinds = new Map(); // chatId -> agentName
|
|
2585
|
+
|
|
2367
2586
|
// Message queue for messages received while a task is running
|
|
2368
2587
|
const messageQueue = new Map(); // chatId -> { messages: string[], notified: false }
|
|
2369
2588
|
|
|
@@ -2482,7 +2701,7 @@ function spawnClaudeStreaming(args, input, cwd, onStatus, timeoutMs = 600000, ch
|
|
|
2482
2701
|
let killed = false;
|
|
2483
2702
|
let finalResult = '';
|
|
2484
2703
|
let lastStatusTime = 0;
|
|
2485
|
-
const STATUS_THROTTLE =
|
|
2704
|
+
const STATUS_THROTTLE = STATUS_THROTTLE_MS;
|
|
2486
2705
|
const writtenFiles = []; // Track files created/modified by Write tool
|
|
2487
2706
|
|
|
2488
2707
|
const timer = setTimeout(() => {
|
|
@@ -2641,7 +2860,6 @@ function spawnClaudeStreaming(args, input, cwd, onStatus, timeoutMs = 600000, ch
|
|
|
2641
2860
|
}
|
|
2642
2861
|
|
|
2643
2862
|
// Lazy distill: run distill.js in background on first message, then every 4 hours
|
|
2644
|
-
let _lastDistillTime = 0;
|
|
2645
2863
|
// Track outbound message_id โ session for reply-based session restoration.
|
|
2646
2864
|
// Keeps last 200 entries to avoid unbounded growth.
|
|
2647
2865
|
function trackMsgSession(messageId, session) {
|
|
@@ -2658,14 +2876,17 @@ function trackMsgSession(messageId, session) {
|
|
|
2658
2876
|
|
|
2659
2877
|
function lazyDistill() {
|
|
2660
2878
|
const now = Date.now();
|
|
2661
|
-
|
|
2879
|
+
const st = loadState();
|
|
2880
|
+
const lastDistillTime = st.last_distill_time || 0;
|
|
2881
|
+
if (now - lastDistillTime < 4 * 60 * 60 * 1000) return; // 4h cooldown
|
|
2662
2882
|
const distillPath = path.join(HOME, '.metame', 'distill.js');
|
|
2663
2883
|
const signalsPath = path.join(HOME, '.metame', 'raw_signals.jsonl');
|
|
2664
2884
|
if (!fs.existsSync(distillPath)) return;
|
|
2665
2885
|
if (!fs.existsSync(signalsPath)) return;
|
|
2666
2886
|
const content = fs.readFileSync(signalsPath, 'utf8').trim();
|
|
2667
2887
|
if (!content) return;
|
|
2668
|
-
|
|
2888
|
+
st.last_distill_time = now;
|
|
2889
|
+
saveState(st);
|
|
2669
2890
|
const lines = content.split('\n').filter(l => l.trim()).length;
|
|
2670
2891
|
log('INFO', `Distilling ${lines} signal(s) in background...`);
|
|
2671
2892
|
const bg = spawn('node', [distillPath], { detached: true, stdio: 'ignore' });
|
|
@@ -2697,16 +2918,8 @@ async function askClaude(bot, chatId, prompt, config) {
|
|
|
2697
2918
|
const agentMatch = routeAgent(prompt, config);
|
|
2698
2919
|
if (agentMatch) {
|
|
2699
2920
|
const { key, proj, rest } = agentMatch;
|
|
2700
|
-
const projCwd =
|
|
2701
|
-
|
|
2702
|
-
const recentInDir = listRecentSessions(1, projCwd);
|
|
2703
|
-
if (recentInDir.length > 0 && recentInDir[0].sessionId) {
|
|
2704
|
-
st.sessions[chatId] = { id: recentInDir[0].sessionId, cwd: projCwd, started: true };
|
|
2705
|
-
} else {
|
|
2706
|
-
const newSess = createSession(chatId, projCwd, proj.name || key);
|
|
2707
|
-
st.sessions[chatId] = { id: newSess.id, cwd: projCwd, started: false };
|
|
2708
|
-
}
|
|
2709
|
-
saveState(st);
|
|
2921
|
+
const projCwd = normalizeCwd(proj.cwd);
|
|
2922
|
+
attachOrCreateSession(chatId, projCwd, proj.name || key);
|
|
2710
2923
|
log('INFO', `Agent switch via nickname: ${key} (${projCwd})`);
|
|
2711
2924
|
if (!rest) {
|
|
2712
2925
|
// Pure nickname call โ confirm switch and stop
|
|
@@ -2770,7 +2983,11 @@ async function askClaude(bot, chatId, prompt, config) {
|
|
|
2770
2983
|
const daemonCfg = loadConfig().daemon || {};
|
|
2771
2984
|
const model = daemonCfg.model || 'opus';
|
|
2772
2985
|
args.push('--model', model);
|
|
2773
|
-
if (
|
|
2986
|
+
if (readOnly) {
|
|
2987
|
+
// Read-only mode for non-operator users: query/chat only, no write/edit/execute
|
|
2988
|
+
const READ_ONLY_TOOLS = ['Read', 'Glob', 'Grep', 'WebSearch', 'WebFetch', 'Task'];
|
|
2989
|
+
for (const tool of READ_ONLY_TOOLS) args.push('--allowedTools', tool);
|
|
2990
|
+
} else if (daemonCfg.dangerously_skip_permissions) {
|
|
2774
2991
|
args.push('--dangerously-skip-permissions');
|
|
2775
2992
|
} else {
|
|
2776
2993
|
const sessionAllowed = daemonCfg.session_allowed_tools || [];
|
|
@@ -2801,14 +3018,22 @@ async function askClaude(bot, chatId, prompt, config) {
|
|
|
2801
3018
|
gitCheckpoint(session.cwd);
|
|
2802
3019
|
|
|
2803
3020
|
// Use streaming mode to show progress
|
|
2804
|
-
// Telegram: edit status msg in-place; Feishu
|
|
3021
|
+
// Telegram: edit status msg in-place; Feishu: edit or fallback to new messages
|
|
3022
|
+
let editFailed = false;
|
|
3023
|
+
let lastFallbackStatus = 0;
|
|
3024
|
+
const FALLBACK_THROTTLE = FALLBACK_THROTTLE_MS;
|
|
2805
3025
|
const onStatus = async (status) => {
|
|
2806
3026
|
try {
|
|
2807
|
-
if (statusMsgId && bot.editMessage) {
|
|
2808
|
-
await bot.editMessage(chatId, statusMsgId, status);
|
|
2809
|
-
|
|
2810
|
-
|
|
3027
|
+
if (statusMsgId && bot.editMessage && !editFailed) {
|
|
3028
|
+
const ok = await bot.editMessage(chatId, statusMsgId, status);
|
|
3029
|
+
if (ok !== false) return; // edit succeeded (true or undefined for Telegram)
|
|
3030
|
+
editFailed = true; // edit failed, switch to fallback permanently
|
|
2811
3031
|
}
|
|
3032
|
+
// Fallback: send as new message with extra throttle to avoid spam
|
|
3033
|
+
const now = Date.now();
|
|
3034
|
+
if (now - lastFallbackStatus < FALLBACK_THROTTLE) return;
|
|
3035
|
+
lastFallbackStatus = now;
|
|
3036
|
+
await bot.sendMessage(chatId, status);
|
|
2812
3037
|
} catch { /* ignore status update failures */ }
|
|
2813
3038
|
};
|
|
2814
3039
|
|
|
@@ -2859,32 +3084,32 @@ async function askClaude(bot, chatId, prompt, config) {
|
|
|
2859
3084
|
recordTokens(loadState(), estimated);
|
|
2860
3085
|
|
|
2861
3086
|
// Parse [[FILE:...]] markers from output (Claude's explicit file sends)
|
|
2862
|
-
const
|
|
2863
|
-
|
|
2864
|
-
|
|
2865
|
-
|
|
2866
|
-
|
|
2867
|
-
|
|
2868
|
-
|
|
2869
|
-
|
|
2870
|
-
|
|
2871
|
-
|
|
2872
|
-
for (const f of files) {
|
|
2873
|
-
if (isContentFile(f)) allFiles.add(f);
|
|
3087
|
+
const { markedFiles, cleanOutput } = parseFileMarkers(output);
|
|
3088
|
+
|
|
3089
|
+
// Match current session to a project for colored card display
|
|
3090
|
+
let activeProject = null;
|
|
3091
|
+
if (session && session.cwd && config && config.projects) {
|
|
3092
|
+
const sessionCwd = path.resolve(normalizeCwd(session.cwd));
|
|
3093
|
+
for (const [, proj] of Object.entries(config.projects)) {
|
|
3094
|
+
if (!proj.cwd) continue;
|
|
3095
|
+
const projCwd = path.resolve(normalizeCwd(proj.cwd));
|
|
3096
|
+
if (sessionCwd === projCwd) { activeProject = proj; break; }
|
|
2874
3097
|
}
|
|
2875
3098
|
}
|
|
2876
3099
|
|
|
2877
|
-
|
|
2878
|
-
if (
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
|
|
2884
|
-
|
|
2885
|
-
|
|
2886
|
-
}
|
|
3100
|
+
let replyMsg;
|
|
3101
|
+
if (activeProject && bot.sendCard) {
|
|
3102
|
+
replyMsg = await bot.sendCard(chatId, {
|
|
3103
|
+
title: `${activeProject.icon || '๐ค'} ${activeProject.name || ''}`,
|
|
3104
|
+
body: cleanOutput,
|
|
3105
|
+
color: activeProject.color || 'blue',
|
|
3106
|
+
});
|
|
3107
|
+
} else {
|
|
3108
|
+
replyMsg = await bot.sendMarkdown(chatId, cleanOutput);
|
|
2887
3109
|
}
|
|
3110
|
+
if (replyMsg && replyMsg.message_id && session) trackMsgSession(replyMsg.message_id, session);
|
|
3111
|
+
|
|
3112
|
+
await sendFileButtons(bot, chatId, mergeFileCollections(markedFiles, files));
|
|
2888
3113
|
|
|
2889
3114
|
// Auto-name: if this was the first message and session has no name, generate one
|
|
2890
3115
|
if (wasNew && !getSessionName(session.id)) {
|
|
@@ -2909,28 +3134,9 @@ async function askClaude(bot, chatId, prompt, config) {
|
|
|
2909
3134
|
const retry = await spawnClaudeStreaming(retryArgs, prompt, session.cwd, onStatus);
|
|
2910
3135
|
if (retry.output) {
|
|
2911
3136
|
markSessionStarted(chatId);
|
|
2912
|
-
|
|
2913
|
-
|
|
2914
|
-
|
|
2915
|
-
const retryCleanOutput = retry.output.replace(/\s*\[\[FILE:[^\]]+\]\]/g, '').trim();
|
|
2916
|
-
await bot.sendMarkdown(chatId, retryCleanOutput);
|
|
2917
|
-
// Combine marked + auto-detected content files
|
|
2918
|
-
const retryAllFiles = new Set(retryMarkedFiles);
|
|
2919
|
-
if (retry.files && retry.files.length > 0) {
|
|
2920
|
-
for (const f of retry.files) {
|
|
2921
|
-
if (isContentFile(f)) retryAllFiles.add(f);
|
|
2922
|
-
}
|
|
2923
|
-
}
|
|
2924
|
-
if (retryAllFiles.size > 0 && bot.sendButtons) {
|
|
2925
|
-
const validFiles = [...retryAllFiles].filter(f => fs.existsSync(f));
|
|
2926
|
-
if (validFiles.length > 0) {
|
|
2927
|
-
const buttons = validFiles.map(filePath => {
|
|
2928
|
-
const shortId = cacheFile(filePath);
|
|
2929
|
-
return [{ text: `๐ ${path.basename(filePath)}`, callback_data: `/file ${shortId}` }];
|
|
2930
|
-
});
|
|
2931
|
-
await bot.sendButtons(chatId, '๐ ๆไปถ:', buttons);
|
|
2932
|
-
}
|
|
2933
|
-
}
|
|
3137
|
+
const { markedFiles: retryMarked, cleanOutput: retryClean } = parseFileMarkers(retry.output);
|
|
3138
|
+
await bot.sendMarkdown(chatId, retryClean);
|
|
3139
|
+
await sendFileButtons(bot, chatId, mergeFileCollections(retryMarked, retry.files));
|
|
2934
3140
|
} else {
|
|
2935
3141
|
log('ERROR', `askClaude retry failed: ${(retry.error || '').slice(0, 200)}`);
|
|
2936
3142
|
try { await bot.sendMessage(chatId, `Error: ${(retry.error || '').slice(0, 200)}`); } catch { /* */ }
|
|
@@ -2975,16 +3181,34 @@ async function startFeishuBridge(config, executeTaskByName) {
|
|
|
2975
3181
|
|
|
2976
3182
|
const { createBot } = require(path.join(__dirname, 'feishu-adapter.js'));
|
|
2977
3183
|
const bot = createBot(config.feishu);
|
|
2978
|
-
const allowedIds = config.feishu.allowed_chat_ids || [];
|
|
2979
|
-
|
|
2980
3184
|
try {
|
|
2981
|
-
const receiver = await bot.startReceiving(async (chatId, text, event, fileInfo) => {
|
|
2982
|
-
// Security: check whitelist (empty = deny all)
|
|
2983
|
-
|
|
3185
|
+
const receiver = await bot.startReceiving(async (chatId, text, event, fileInfo, senderId) => {
|
|
3186
|
+
// Security: check whitelist (empty = deny all) โ read live config to support hot-reload
|
|
3187
|
+
// Exception: /bind is allowed from any chat so users can self-register new groups
|
|
3188
|
+
const liveCfg = loadConfig();
|
|
3189
|
+
const allowedIds = (liveCfg.feishu && liveCfg.feishu.allowed_chat_ids) || [];
|
|
3190
|
+
const isBindCmd = text && text.trim().startsWith('/bind');
|
|
3191
|
+
if (!allowedIds.includes(chatId) && !isBindCmd) {
|
|
2984
3192
|
log('WARN', `Feishu: rejected message from ${chatId}`);
|
|
2985
3193
|
return;
|
|
2986
3194
|
}
|
|
2987
3195
|
|
|
3196
|
+
// Operator check: if operator_ids configured, non-operators get read-only chat mode
|
|
3197
|
+
const operatorIds = (liveCfg.feishu && liveCfg.feishu.operator_ids) || [];
|
|
3198
|
+
if (operatorIds.length > 0 && senderId && !operatorIds.includes(senderId) && !isBindCmd) {
|
|
3199
|
+
log('INFO', `Feishu: read-only message from non-operator ${senderId} in ${chatId}: ${(text || '').slice(0, 50)}`);
|
|
3200
|
+
// Block slash commands for non-operators
|
|
3201
|
+
if (text && text.startsWith('/')) {
|
|
3202
|
+
await bot.sendMessage(chatId, 'โ ๏ธ ่ฏฅๆไฝ้่ฆๆๆ๏ผ่ฏท่็ณป็ฎก็ๅใ');
|
|
3203
|
+
return;
|
|
3204
|
+
}
|
|
3205
|
+
// Allow read-only chat (query/answer only, no write/edit/execute)
|
|
3206
|
+
if (text) {
|
|
3207
|
+
await handleCommand(bot, chatId, text, config, executeTaskByName, senderId, true);
|
|
3208
|
+
}
|
|
3209
|
+
return;
|
|
3210
|
+
}
|
|
3211
|
+
|
|
2988
3212
|
// Handle file message
|
|
2989
3213
|
if (fileInfo && fileInfo.fileKey) {
|
|
2990
3214
|
log('INFO', `Feishu file from ${chatId}: ${fileInfo.fileName} (key: ${fileInfo.fileKey}, msgId: ${fileInfo.messageId}, type: ${fileInfo.msgType})`);
|
|
@@ -3027,7 +3251,7 @@ async function startFeishuBridge(config, executeTaskByName) {
|
|
|
3027
3251
|
log('INFO', `Session restored via reply: ${mapped.id.slice(0, 8)} (${path.basename(mapped.cwd)})`);
|
|
3028
3252
|
}
|
|
3029
3253
|
}
|
|
3030
|
-
await handleCommand(bot, chatId, text, config, executeTaskByName);
|
|
3254
|
+
await handleCommand(bot, chatId, text, config, executeTaskByName, senderId);
|
|
3031
3255
|
}
|
|
3032
3256
|
});
|
|
3033
3257
|
|
package/scripts/distill.js
CHANGED
|
@@ -159,6 +159,7 @@ RULES:
|
|
|
159
159
|
5. Episodic exceptions: context.anti_patterns (max 5, cross-project lessons only), context.milestones (max 3).
|
|
160
160
|
6. Strong directives (ไปฅๅไธๅพ/always/never/from now on) โ _confidence: high. Otherwise: normal.
|
|
161
161
|
7. Add _confidence and _source blocks mapping field keys to confidence level and triggering quote.
|
|
162
|
+
8. NEVER extract agent identity or role definitions. Messages like "ไฝ ๆฏ่ดพ็ปดๆฏ/ไฝ ็่ง่ฒๆฏ.../you are Jarvis" define the AGENT, not the USER. The profile is about the USER's cognition only.
|
|
162
163
|
|
|
163
164
|
BIAS PREVENTION:
|
|
164
165
|
- Single observation = STATE, not TRAIT. T3 cognition needs 3+ observations.
|
|
@@ -51,13 +51,22 @@ function createBot(config) {
|
|
|
51
51
|
return msgId ? { message_id: msgId } : null;
|
|
52
52
|
},
|
|
53
53
|
|
|
54
|
+
_editBroken: false, // Set to true if patch API consistently fails
|
|
54
55
|
async editMessage(chatId, messageId, text) {
|
|
56
|
+
if (this._editBroken) return false;
|
|
55
57
|
try {
|
|
56
58
|
await client.im.message.patch({
|
|
57
59
|
path: { message_id: messageId },
|
|
58
60
|
data: { content: JSON.stringify({ text }) },
|
|
59
61
|
});
|
|
60
|
-
|
|
62
|
+
return true;
|
|
63
|
+
} catch (e) {
|
|
64
|
+
const code = e?.code || e?.response?.data?.code;
|
|
65
|
+
if (code === 230001 || code === 230002 || /permission|forbidden/i.test(String(e))) {
|
|
66
|
+
this._editBroken = true;
|
|
67
|
+
}
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
61
70
|
},
|
|
62
71
|
|
|
63
72
|
/**
|
|
@@ -69,13 +78,12 @@ function createBot(config) {
|
|
|
69
78
|
.replace(/^(#{1,3})\s+(.+)$/gm, '**$2**') // headers โ bold
|
|
70
79
|
.replace(/^---+$/gm, 'โโโโโโโโโโโโโโโโโโโโโ'); // hr โ unicode line
|
|
71
80
|
|
|
72
|
-
// Split into chunks if too long (
|
|
81
|
+
// Split into chunks if too long (element limit ~4000 chars)
|
|
73
82
|
const MAX_CHUNK = 3800;
|
|
74
83
|
const chunks = [];
|
|
75
84
|
if (content.length <= MAX_CHUNK) {
|
|
76
85
|
chunks.push(content);
|
|
77
86
|
} else {
|
|
78
|
-
// Split on double newlines to avoid breaking mid-paragraph
|
|
79
87
|
const paragraphs = content.split(/\n\n/);
|
|
80
88
|
let buf = '';
|
|
81
89
|
for (const p of paragraphs) {
|
|
@@ -89,14 +97,16 @@ function createBot(config) {
|
|
|
89
97
|
if (buf) chunks.push(buf);
|
|
90
98
|
}
|
|
91
99
|
|
|
100
|
+
// V2 schema: markdown element with normal text size
|
|
92
101
|
const elements = chunks.map(c => ({
|
|
93
|
-
tag: '
|
|
94
|
-
|
|
102
|
+
tag: 'markdown',
|
|
103
|
+
content: c,
|
|
104
|
+
text_size: 'x-large',
|
|
95
105
|
}));
|
|
96
106
|
|
|
97
107
|
const card = {
|
|
98
|
-
|
|
99
|
-
elements,
|
|
108
|
+
schema: '2.0',
|
|
109
|
+
body: { elements },
|
|
100
110
|
};
|
|
101
111
|
|
|
102
112
|
const res = await client.im.message.create({
|
|
@@ -119,11 +129,56 @@ function createBot(config) {
|
|
|
119
129
|
* @param {string} color - header color: blue|orange|green|red|grey|purple|turquoise
|
|
120
130
|
*/
|
|
121
131
|
async sendCard(chatId, { title, body, color = 'blue' }) {
|
|
122
|
-
|
|
132
|
+
// Use card schema V2 for better text sizing
|
|
133
|
+
if (!body) {
|
|
134
|
+
const card = {
|
|
135
|
+
schema: '2.0',
|
|
136
|
+
header: { title: { tag: 'plain_text', content: title }, template: color },
|
|
137
|
+
body: { elements: [] },
|
|
138
|
+
};
|
|
139
|
+
const res = await client.im.message.create({
|
|
140
|
+
params: { receive_id_type: 'chat_id' },
|
|
141
|
+
data: { receive_id: chatId, msg_type: 'interactive', content: JSON.stringify(card) },
|
|
142
|
+
});
|
|
143
|
+
const msgId = res?.data?.message_id;
|
|
144
|
+
return msgId ? { message_id: msgId } : null;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Convert standard markdown โ lark_md
|
|
148
|
+
let content = body
|
|
149
|
+
.replace(/^(#{1,3})\s+(.+)$/gm, '**$2**')
|
|
150
|
+
.replace(/^---+$/gm, 'โโโโโโโโโโโโโโโโโโโโโ');
|
|
151
|
+
|
|
152
|
+
// Split into chunks (lark_md element limit ~4000 chars)
|
|
153
|
+
const MAX_CHUNK = 3800;
|
|
154
|
+
const chunks = [];
|
|
155
|
+
if (content.length <= MAX_CHUNK) {
|
|
156
|
+
chunks.push(content);
|
|
157
|
+
} else {
|
|
158
|
+
const paragraphs = content.split(/\n\n/);
|
|
159
|
+
let buf = '';
|
|
160
|
+
for (const p of paragraphs) {
|
|
161
|
+
if (buf.length + p.length + 2 > MAX_CHUNK && buf) {
|
|
162
|
+
chunks.push(buf);
|
|
163
|
+
buf = p;
|
|
164
|
+
} else {
|
|
165
|
+
buf = buf ? buf + '\n\n' + p : p;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
if (buf) chunks.push(buf);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// V2: use markdown element with text_size for readable font
|
|
172
|
+
const elements = chunks.map(c => ({
|
|
173
|
+
tag: 'markdown',
|
|
174
|
+
content: c,
|
|
175
|
+
text_size: 'x-large',
|
|
176
|
+
}));
|
|
177
|
+
|
|
123
178
|
const card = {
|
|
124
|
-
|
|
179
|
+
schema: '2.0',
|
|
125
180
|
header: { title: { tag: 'plain_text', content: title }, template: color },
|
|
126
|
-
elements,
|
|
181
|
+
body: { elements },
|
|
127
182
|
};
|
|
128
183
|
const res = await client.im.message.create({
|
|
129
184
|
params: { receive_id_type: 'chat_id' },
|
|
@@ -133,6 +188,15 @@ function createBot(config) {
|
|
|
133
188
|
return msgId ? { message_id: msgId } : null;
|
|
134
189
|
},
|
|
135
190
|
|
|
191
|
+
/**
|
|
192
|
+
* Delete a message by ID
|
|
193
|
+
*/
|
|
194
|
+
async deleteMessage(chatId, messageId) {
|
|
195
|
+
try {
|
|
196
|
+
await client.im.message.delete({ path: { message_id: messageId } });
|
|
197
|
+
} catch { /* non-fatal โ message may already be deleted or expired */ }
|
|
198
|
+
},
|
|
199
|
+
|
|
136
200
|
/**
|
|
137
201
|
* Typing indicator (Feishu has no such API โ no-op)
|
|
138
202
|
*/
|
|
@@ -342,6 +406,7 @@ function createBot(config) {
|
|
|
342
406
|
if (isDuplicate(msg.message_id)) return;
|
|
343
407
|
|
|
344
408
|
const chatId = msg.chat_id;
|
|
409
|
+
const senderId = data.sender && data.sender.sender_id && data.sender.sender_id.open_id || null;
|
|
345
410
|
let text = '';
|
|
346
411
|
let fileInfo = null;
|
|
347
412
|
|
|
@@ -370,7 +435,7 @@ function createBot(config) {
|
|
|
370
435
|
|
|
371
436
|
if (text || fileInfo) {
|
|
372
437
|
// Fire-and-forget: don't block the event loop (SDK needs fast ack)
|
|
373
|
-
Promise.resolve().then(() => onMessage(chatId, text, data, fileInfo)).catch(() => {});
|
|
438
|
+
Promise.resolve().then(() => onMessage(chatId, text, data, fileInfo, senderId)).catch(() => {});
|
|
374
439
|
}
|
|
375
440
|
} catch (e) {
|
|
376
441
|
// Non-fatal
|
package/scripts/schema.js
CHANGED
|
@@ -16,8 +16,7 @@
|
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
18
|
const SCHEMA = {
|
|
19
|
-
// === T1: Identity ===
|
|
20
|
-
'identity.nickname': { tier: 'T1', type: 'string', locked: true },
|
|
19
|
+
// === T1: Identity (USER's identity, not agent's) ===
|
|
21
20
|
'identity.role': { tier: 'T1', type: 'string', locked: false },
|
|
22
21
|
'identity.locale': { tier: 'T1', type: 'string', locked: true },
|
|
23
22
|
|
|
@@ -65,6 +65,11 @@ process.stdin.on('end', () => {
|
|
|
65
65
|
process.exit(0);
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
+
// Skip agent identity definitions (these belong in project CLAUDE.md, not user profile)
|
|
69
|
+
if (/^(ไฝ ๆฏ|ไฝ ๅซ|ไฝ ็(่ง่ฒ|่บซไปฝ|่่ดฃ|ไปปๅก)|ไฝ ่ด่ดฃ|ไฝ ็ฐๅจๆฏ|from now on you are|you are now|your role is)/i.test(prompt)) {
|
|
70
|
+
process.exit(0);
|
|
71
|
+
}
|
|
72
|
+
|
|
68
73
|
// Skip pasted error logs / stack traces
|
|
69
74
|
if (/^(Error|TypeError|SyntaxError|ReferenceError|at\s+\w+|Traceback|FATAL|WARN|ERR!)/i.test(prompt)) {
|
|
70
75
|
process.exit(0);
|