comisai 1.0.9 → 1.0.11

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 (30) hide show
  1. package/README.md +3 -3
  2. package/node_modules/@comis/agent/dist/executor/executor-post-execution.js +2 -1
  3. package/node_modules/@comis/agent/dist/executor/executor-response-filter.js +33 -0
  4. package/node_modules/@comis/agent/dist/executor/pi-executor.d.ts +4 -1
  5. package/node_modules/@comis/agent/dist/executor/prompt-assembly.d.ts +5 -0
  6. package/node_modules/@comis/agent/dist/executor/prompt-assembly.js +20 -1
  7. package/node_modules/@comis/agent/dist/executor/schema-stripping.d.ts +1 -1
  8. package/node_modules/@comis/agent/dist/executor/schema-stripping.js +8 -1
  9. package/node_modules/@comis/agent/dist/executor/session-snapshot-cleanup.js +2 -1
  10. package/node_modules/@comis/agent/package.json +1 -1
  11. package/node_modules/@comis/channels/package.json +1 -1
  12. package/node_modules/@comis/cli/dist/commands/agent.js +19 -27
  13. package/node_modules/@comis/cli/dist/commands/models.js +2 -2
  14. package/node_modules/@comis/cli/dist/commands/reset.js +19 -3
  15. package/node_modules/@comis/cli/dist/wizard/steps/06-channels.js +88 -11
  16. package/node_modules/@comis/cli/dist/wizard/steps/10-write-config.js +4 -0
  17. package/node_modules/@comis/cli/dist/wizard/types.d.ts +1 -0
  18. package/node_modules/@comis/cli/package.json +1 -1
  19. package/node_modules/@comis/core/package.json +1 -1
  20. package/node_modules/@comis/daemon/package.json +1 -1
  21. package/node_modules/@comis/gateway/package.json +1 -1
  22. package/node_modules/@comis/infra/package.json +1 -1
  23. package/node_modules/@comis/memory/package.json +1 -1
  24. package/node_modules/@comis/scheduler/package.json +1 -1
  25. package/node_modules/@comis/shared/package.json +1 -1
  26. package/node_modules/@comis/skills/dist/builtin/exec-security.js +2 -2
  27. package/node_modules/@comis/skills/dist/builtin/exec-tool.js +1 -1
  28. package/node_modules/@comis/skills/dist/integrations/media-handler-audio.js +1 -1
  29. package/node_modules/@comis/skills/package.json +1 -1
  30. package/package.json +12 -12
package/README.md CHANGED
@@ -9,13 +9,13 @@
9
9
  <p align="center">
10
10
  <a href="https://www.npmjs.com/package/comisai"><img src="https://img.shields.io/npm/v/comisai" alt="npm version" /></a>
11
11
  <a href="https://github.com/comisai/comis/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-Apache--2.0-06B6D4?style=flat" alt="License" /></a>
12
- <a href="https://discord.gg/comis"><img src="https://img.shields.io/badge/discord-join-5865F2?style=flat&logo=discord&logoColor=white" alt="Discord" /></a>
12
+ <a href="https://discord.gg/FsqgJkpp"><img src="https://img.shields.io/badge/discord-join-5865F2?style=flat&logo=discord&logoColor=white" alt="Discord" /></a>
13
13
  <a href="https://nodejs.org"><img src="https://img.shields.io/node/v/comisai" alt="Node" /></a>
14
14
  </p>
15
15
 
16
16
  <p align="center">
17
17
  <a href="https://docs.comis.ai">Docs</a> &middot;
18
- <a href="https://discord.gg/comis">Discord</a> &middot;
18
+ <a href="https://discord.gg/FsqgJkpp">Discord</a> &middot;
19
19
  <a href="https://twitter.com/comis_ai">Twitter</a> &middot;
20
20
  <a href="#quick-start">Quick Start</a>
21
21
  </p>
@@ -168,7 +168,7 @@ shared (Result type, utilities)
168
168
  <p align="center">
169
169
  <a href="https://github.com/comisai/comis"><strong>GitHub</strong></a> &middot;
170
170
  <a href="https://docs.comis.ai"><strong>Docs</strong></a> &middot;
171
- <a href="https://discord.gg/comis"><strong>Discord</strong></a> &middot;
171
+ <a href="https://discord.gg/FsqgJkpp"><strong>Discord</strong></a> &middot;
172
172
  <a href="https://twitter.com/comis_ai"><strong>Twitter</strong></a> &middot;
173
173
  <a href="https://github.com/comisai/comis/issues"><strong>Issues</strong></a>
174
174
  </p>
@@ -332,6 +332,7 @@ export async function postExecution(params) {
332
332
  // Strip verbose <functions> blocks from discover_tools results
333
333
  // in session history. Runs post-execution so the current turn's model
334
334
  // saw full schemas. Safe no-op when no discover_tools results exist.
335
- stripDiscoverySchemas(sm, deps.logger);
335
+ // Fence-aware: entries at or below the cache fence are not stripped to preserve prefix stability.
336
+ stripDiscoverySchemas(sm, deps.logger, finalBreakpointIdx);
336
337
  session.dispose();
337
338
  }
@@ -85,6 +85,11 @@ export function scanWithOutputGuard(params) {
85
85
  // ---------------------------------------------------------------------------
86
86
  /** Silent tokens that indicate the final message has no visible content. */
87
87
  const SILENT_FINAL_TOKENS = ["NO_REPLY", "HEARTBEAT_OK"];
88
+ /** Tool names whose successful invocation means the agent already delivered
89
+ * content to the user through a side-channel. When one of these tools was
90
+ * called during the execution, a silent final token (NO_REPLY) is intentional
91
+ * and recovery must be suppressed to avoid leaking internal narration. */
92
+ const DELIVERY_TOOL_NAMES = ["message", "notify"];
88
93
  /**
89
94
  * When the final assistant message is thinking-only or a
90
95
  * silent token (NO_REPLY, HEARTBEAT_OK) but text was emitted in earlier
@@ -107,6 +112,17 @@ export function recoverEmptyFinalResponse(params) {
107
112
  const isSilentFinalToken = SILENT_FINAL_TOKENS.includes(extractedResponse.trim());
108
113
  if ((extractedResponse === "" || isSilentFinalToken) && textEmitted) {
109
114
  if (Array.isArray(messages)) {
115
+ // Guard: if the agent already delivered content via a delivery tool
116
+ // (message, notify), the silent final token is intentional — skip
117
+ // recovery to avoid leaking internal narration (e.g. "Now let me
118
+ // generate the chart:" surfaced as a user-visible message).
119
+ if (isSilentFinalToken && hasDeliveryToolCall(messages, lowerBound)) {
120
+ logger.debug({
121
+ hint: "Silent final token after delivery tool call — recovery suppressed",
122
+ extractedResponse,
123
+ }, "Skipping empty-response recovery (delivery tool used)");
124
+ return extractedResponse;
125
+ }
110
126
  /* eslint-disable @typescript-eslint/no-explicit-any */
111
127
  // Pass 1: backward walk — prefer the most recent standalone response
112
128
  // (assistant turns that have text but NO tool call blocks)
@@ -172,6 +188,23 @@ function extractVisibleText(content) {
172
188
  return undefined;
173
189
  }
174
190
  /* eslint-enable @typescript-eslint/no-explicit-any */
191
+ /** Check whether any assistant turn (from lowerBound onward) contains a tool
192
+ * call to a delivery tool (message, notify). These tools send content to the
193
+ * user through a side-channel, so a subsequent NO_REPLY is intentional. */
194
+ /* eslint-disable @typescript-eslint/no-explicit-any */
195
+ function hasDeliveryToolCall(messages, lowerBound) {
196
+ for (let i = lowerBound; i < messages.length; i++) {
197
+ const msg = messages[i]; // eslint-disable-line security/detect-object-injection
198
+ if (msg?.role === "assistant" && Array.isArray(msg.content)) {
199
+ const hasDelivery = msg.content.some((b) => (b?.type === "toolCall" || b?.type === "tool_use") &&
200
+ DELIVERY_TOOL_NAMES.includes(b?.name));
201
+ if (hasDelivery)
202
+ return true;
203
+ }
204
+ }
205
+ return false;
206
+ }
207
+ /* eslint-enable @typescript-eslint/no-explicit-any */
175
208
  // ---------------------------------------------------------------------------
176
209
  // SEP plan extraction (extracted from execute() success path)
177
210
  // ---------------------------------------------------------------------------
@@ -50,7 +50,10 @@ import type { AgentExecutor } from "./types.js";
50
50
  *
51
51
  * Extracted as a named function for independent unit testing.
52
52
  */
53
- export declare function createBeforeToolCallGuard(stepCounter: StepCounter, budgetGuard: BudgetGuard, circuitBreaker: CircuitBreaker, toolRetryBreaker?: ToolRetryBreaker, messageSendLimiter?: MessageSendLimiter): (context: unknown, _signal?: AbortSignal) => Promise<import("../safety/message-send-limiter.js").MessageSendVerdict | undefined>;
53
+ export declare function createBeforeToolCallGuard(stepCounter: StepCounter, budgetGuard: BudgetGuard, circuitBreaker: CircuitBreaker, toolRetryBreaker?: ToolRetryBreaker, messageSendLimiter?: MessageSendLimiter): (context: unknown, _signal?: AbortSignal) => Promise<{
54
+ block: boolean;
55
+ reason: string;
56
+ } | undefined>;
54
57
  /**
55
58
  * Merge SDK session stats into execution result for token totals (R-13).
56
59
  *
@@ -46,6 +46,11 @@ export declare function clearSessionToolNameSnapshot(sessionKey: string): void;
46
46
  * Call during session cleanup to prevent the Map from growing unbounded.
47
47
  */
48
48
  export declare function clearSessionBootstrapFileSnapshot(sessionKey: string): void;
49
+ /**
50
+ * Clear the cached prompt skills XML snapshot for a session.
51
+ * Call during session cleanup to prevent the Map from growing unbounded.
52
+ */
53
+ export declare function clearSessionPromptSkillsXmlSnapshot(sessionKey: string): void;
49
54
  /** Frozen prompt state captured after first-turn assembly for sub-agent cache prefix sharing.
50
55
  * When propagated to sub-agents via SpawnPacket, allows prefix reuse instead of independent assembly. */
51
56
  export interface CacheSafeParams {
@@ -57,6 +57,11 @@ const sessionBootstrapFileSnapshots = new Map();
57
57
  * Captured once per session at the end of first assembleExecutionPrompt call.
58
58
  * Sub-agents read this via getCacheSafeParams() to reuse parent prefix. */
59
59
  const sessionCacheSafeParams = new Map();
60
+ /** Per-session prompt skills XML snapshot for stable system prompt assembly.
61
+ * On first execution, captures the promptSkillsXml string. Subsequent executions
62
+ * reuse the snapshot so skills XML fed to assembleRichSystemPrompt stays constant,
63
+ * preventing cache-invalidating changes when the agent creates skills mid-session. */
64
+ const sessionPromptSkillsXmlSnapshots = new Map();
60
65
  // ---------------------------------------------------------------------------
61
66
  // Feature flag hash for tool cache key invalidation.
62
67
  // Computes a stable string from config fields that affect tool rendering.
@@ -95,6 +100,13 @@ export function clearSessionToolNameSnapshot(sessionKey) {
95
100
  export function clearSessionBootstrapFileSnapshot(sessionKey) {
96
101
  sessionBootstrapFileSnapshots.delete(sessionKey);
97
102
  }
103
+ /**
104
+ * Clear the cached prompt skills XML snapshot for a session.
105
+ * Call during session cleanup to prevent the Map from growing unbounded.
106
+ */
107
+ export function clearSessionPromptSkillsXmlSnapshot(sessionKey) {
108
+ sessionPromptSkillsXmlSnapshots.delete(sessionKey);
109
+ }
98
110
  /**
99
111
  * Get the frozen prompt state for a session (sub-agent cache prefix sharing).
100
112
  * Returns undefined if no params captured yet (session hasn't completed first turn).
@@ -458,7 +470,14 @@ export async function assembleExecutionPrompt(params) {
458
470
  sessionToolNameSnapshots.set(snapshotKey, toolNames);
459
471
  }
460
472
  const hasMemoryTools = stableToolNames.includes("memory_store") || stableToolNames.includes("memory_search");
461
- const promptSkillsXml = deps.getPromptSkillsXml?.() ?? undefined;
473
+ // Snapshot promptSkillsXml on first turn to keep system prompt stable.
474
+ // Skills created mid-session grow the XML (~540 chars per skill), invalidating
475
+ // the entire system prompt cache prefix on every subsequent turn.
476
+ let promptSkillsXml = sessionPromptSkillsXmlSnapshots.get(snapshotKey);
477
+ if (promptSkillsXml === undefined && !sessionPromptSkillsXmlSnapshots.has(snapshotKey)) {
478
+ promptSkillsXml = deps.getPromptSkillsXml?.() ?? undefined;
479
+ sessionPromptSkillsXmlSnapshots.set(snapshotKey, promptSkillsXml);
480
+ }
462
481
  const activePromptSkillContent = msg.metadata?.promptSkillContent;
463
482
  // Extract user's preferred language from USER.md (if present)
464
483
  const userLanguage = extractUserLanguage(bootstrapContextFiles);
@@ -16,4 +16,4 @@
16
16
  */
17
17
  export declare function stripDiscoverySchemas(sm: unknown, logger?: {
18
18
  debug: (obj: Record<string, unknown>, msg: string) => void;
19
- }): void;
19
+ }, cacheFenceIndex?: number): void;
@@ -26,15 +26,19 @@
26
26
  * @param sm - SessionManager instance (from withSession callback)
27
27
  * @param logger - Optional logger for DEBUG-level stripping stats
28
28
  */
29
- export function stripDiscoverySchemas(sm, logger) {
29
+ export function stripDiscoverySchemas(sm, logger, cacheFenceIndex) {
30
30
  /* eslint-disable @typescript-eslint/no-explicit-any */
31
31
  const sessionManager = sm;
32
32
  const fileEntries = sessionManager.fileEntries;
33
33
  if (!Array.isArray(fileEntries))
34
34
  return;
35
+ const fence = cacheFenceIndex ?? -1;
35
36
  let strippedCount = 0;
36
37
  let totalCharsSaved = 0;
38
+ let messageIdx = -1;
37
39
  for (const entry of fileEntries) {
40
+ if (entry.type === "message")
41
+ messageIdx++;
38
42
  if (entry.type !== "message")
39
43
  continue;
40
44
  const msg = entry.message;
@@ -42,6 +46,9 @@ export function stripDiscoverySchemas(sm, logger) {
42
46
  continue;
43
47
  if (msg.toolName !== "discover_tools")
44
48
  continue;
49
+ // Entries at or below the cache fence must not be modified to preserve prefix stability
50
+ if (messageIdx <= fence)
51
+ continue;
45
52
  const content = msg.content;
46
53
  if (!Array.isArray(content) || content.length === 0)
47
54
  continue;
@@ -14,7 +14,7 @@
14
14
  * @module
15
15
  */
16
16
  import { formatSessionKey } from "@comis/core";
17
- import { clearSessionToolNameSnapshot, clearSessionBootstrapFileSnapshot, clearCacheSafeParams } from "./prompt-assembly.js";
17
+ import { clearSessionToolNameSnapshot, clearSessionBootstrapFileSnapshot, clearSessionPromptSkillsXmlSnapshot, clearCacheSafeParams } from "./prompt-assembly.js";
18
18
  import { clearSessionDeliveredGuides, clearSessionToolSchemaSnapshot, clearSessionToolSchemaSnapshotHash, clearSessionBreakpointIndex, clearSessionCacheWarm, clearSessionLatches, clearSessionEvictionCooldown, clearSessionCacheSavings } from "./executor-session-state.js";
19
19
  import { clearSessionTracker } from "./tool-lifecycle.js";
20
20
  import { clearDiscoveryTracker } from "./discovery-tracker.js";
@@ -31,6 +31,7 @@ import { clearSessionBlockStability } from "./block-stability-tracker.js";
31
31
  export function clearSessionState(formattedKey) {
32
32
  clearSessionToolNameSnapshot(formattedKey);
33
33
  clearSessionBootstrapFileSnapshot(formattedKey);
34
+ clearSessionPromptSkillsXmlSnapshot(formattedKey);
34
35
  clearCacheSafeParams(formattedKey);
35
36
  clearSessionDeliveredGuides(formattedKey);
36
37
  clearSessionToolSchemaSnapshot(formattedKey);
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@comis/agent",
3
3
  "private": true,
4
- "version": "1.0.9",
4
+ "version": "1.0.11",
5
5
  "author": "Moshe Anconina",
6
6
  "license": "Apache-2.0",
7
7
  "description": "AI agent executor, budget control, and session management for Comis",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@comis/channels",
3
3
  "private": true,
4
- "version": "1.0.9",
4
+ "version": "1.0.11",
5
5
  "author": "Moshe Anconina",
6
6
  "license": "Apache-2.0",
7
7
  "description": "Chat platform adapters — Discord, Telegram, Slack, WhatsApp, Signal, iMessage, IRC, LINE",
@@ -28,9 +28,7 @@ export function registerAgentCommand(program) {
28
28
  .action(async (options) => {
29
29
  try {
30
30
  const result = await withSpinner("Fetching agents...", () => withClient(async (client) => {
31
- const agentsResult = (await client.call("config.get", { section: "agents" }));
32
- const routingResult = (await client.call("config.get", { section: "routing" }));
33
- return { ...agentsResult, ...routingResult };
31
+ return (await client.call("config.get", { section: "agents" }));
34
32
  }));
35
33
  const agents = extractAgents(result);
36
34
  if (agents.length === 0) {
@@ -64,14 +62,13 @@ export function registerAgentCommand(program) {
64
62
  try {
65
63
  const agentConfig = { name };
66
64
  if (options.provider)
67
- agentConfig["defaultProvider"] = options.provider;
65
+ agentConfig["provider"] = options.provider;
68
66
  if (options.model)
69
- agentConfig["defaultModel"] = options.model;
67
+ agentConfig["model"] = options.model;
70
68
  await withSpinner(`Creating agent "${name}"...`, () => withClient(async (client) => {
71
- return await client.call("config.set", {
72
- section: "routing",
73
- key: `agents.${name}`,
74
- value: agentConfig,
69
+ return await client.call("agents.create", {
70
+ agentId: name,
71
+ config: agentConfig,
75
72
  });
76
73
  }));
77
74
  // Initialize dedicated workspace for the new agent
@@ -109,14 +106,13 @@ export function registerAgentCommand(program) {
109
106
  try {
110
107
  const updates = {};
111
108
  if (options.provider)
112
- updates["defaultProvider"] = options.provider;
109
+ updates["provider"] = options.provider;
113
110
  if (options.model)
114
- updates["defaultModel"] = options.model;
111
+ updates["model"] = options.model;
115
112
  await withSpinner(`Updating agent "${name}"...`, () => withClient(async (client) => {
116
- return await client.call("config.set", {
117
- section: "routing",
118
- key: `agents.${name}`,
119
- value: updates,
113
+ return await client.call("agents.update", {
114
+ agentId: name,
115
+ config: updates,
120
116
  });
121
117
  }));
122
118
  success(`Agent "${name}" updated`);
@@ -162,10 +158,8 @@ export function registerAgentCommand(program) {
162
158
  }
163
159
  try {
164
160
  await withSpinner(`Deleting agent "${name}"...`, () => withClient(async (client) => {
165
- return await client.call("config.set", {
166
- section: "routing",
167
- key: `agents.${name}`,
168
- value: null,
161
+ return await client.call("agents.delete", {
162
+ agentId: name,
169
163
  });
170
164
  }));
171
165
  success(`Agent "${name}" deleted`);
@@ -223,16 +217,16 @@ export function registerAgentCommand(program) {
223
217
  });
224
218
  }
225
219
  /**
226
- * Extract agent entries from routing config response.
220
+ * Extract agent entries from config.get response for the agents section.
227
221
  *
228
222
  * Handles various response shapes from the config.get RPC call,
229
223
  * normalizing into a flat array of AgentEntry objects.
230
224
  */
231
225
  function extractAgents(config) {
226
+ if (!config || typeof config !== "object")
227
+ return [];
232
228
  const agents = [];
233
- // Try routing.agents or agents directly
234
- const agentsObj = config["agents"] ??
235
- config["routing"]?.["agents"];
229
+ const agentsObj = config["agents"];
236
230
  if (agentsObj && typeof agentsObj === "object") {
237
231
  for (const [name, value] of Object.entries(agentsObj)) {
238
232
  if (value && typeof value === "object") {
@@ -250,10 +244,8 @@ function extractAgents(config) {
250
244
  }
251
245
  }
252
246
  }
253
- // Also check top-level or routing-nested bindings for agent references
254
- const routing = config["routing"];
255
- const bindings = config["bindings"] ??
256
- routing?.["bindings"];
247
+ // Also check bindings for agent references
248
+ const bindings = config["bindings"];
257
249
  if (Array.isArray(bindings)) {
258
250
  for (const binding of bindings) {
259
251
  const agentId = typeof binding["agentId"] === "string" ? binding["agentId"] : undefined;
@@ -180,8 +180,8 @@ export function registerModelsCommand(program) {
180
180
  return; // Unreachable but satisfies TS control flow
181
181
  }
182
182
  // Find the agent entry
183
- const agentsNode = doc.getIn(["agents"]) ?? doc.getIn(["routing", "agents"]);
184
- const agentsPath = doc.getIn(["agents"]) ? ["agents"] : ["routing", "agents"];
183
+ const agentsNode = doc.getIn(["agents"]);
184
+ const agentsPath = ["agents"];
185
185
  if (!agentsNode || typeof agentsNode !== "object") {
186
186
  error("No agents section found in config");
187
187
  info("Configure agents first with 'comis init' or edit config manually");
@@ -67,20 +67,36 @@ function safeUnlink(filePath) {
67
67
  }
68
68
  }
69
69
  }
70
+ /**
71
+ * Check whether an error indicates the daemon is unreachable (not running,
72
+ * wrong port, connection refused) vs. a method-level RPC error.
73
+ */
74
+ function isDaemonUnreachable(err) {
75
+ const msg = err instanceof Error ? err.message : String(err);
76
+ return msg.includes("Cannot connect to daemon") || msg.includes("timed out") || msg.includes("ECONNREFUSED");
77
+ }
70
78
  /**
71
79
  * Execute reset for sessions target.
72
80
  *
73
- * Tries RPC-based session deletion first (daemon running).
81
+ * Tries RPC-based session deletion first (daemon running): lists all sessions
82
+ * via session.list, then deletes each via session.delete.
74
83
  * Falls back to direct SQLite database file removal if daemon is not running.
75
84
  */
76
85
  async function resetSessions(dataDir) {
77
86
  try {
78
87
  await withSpinner("Clearing sessions via daemon...", () => withClient(async (client) => {
79
- return await client.call("session.reset");
88
+ const result = await client.call("session.list");
89
+ if (result.total === 0)
90
+ return;
91
+ for (const session of result.sessions) {
92
+ await client.call("session.delete", { session_key: session.sessionKey });
93
+ }
80
94
  }));
81
95
  success("All sessions deleted");
82
96
  }
83
- catch {
97
+ catch (err) {
98
+ if (!isDaemonUnreachable(err))
99
+ throw err;
84
100
  // Daemon not running -- try direct file deletion
85
101
  const dbPath = dataDir + "/memory.db";
86
102
  try {
@@ -130,6 +130,37 @@ async function validateLineLive(channelToken) {
130
130
  clearTimeout(timeout);
131
131
  }
132
132
  }
133
+ /**
134
+ * Fetch the most recent sender's user ID from the bot's getUpdates endpoint.
135
+ *
136
+ * Calls getUpdates with a short timeout. Returns the first message sender's
137
+ * user ID and name, or null if no messages are available.
138
+ */
139
+ async function fetchTelegramSenderId(token) {
140
+ const controller = new AbortController();
141
+ const timeout = setTimeout(() => controller.abort(), 5000);
142
+ try {
143
+ const response = await fetch(`https://api.telegram.org/bot${token}/getUpdates?limit=5&allowed_updates=["message"]`, { method: "GET", signal: controller.signal });
144
+ if (!response.ok)
145
+ return null;
146
+ const data = (await response.json());
147
+ if (!data.ok || !data.result.length)
148
+ return null;
149
+ for (const update of data.result.reverse()) {
150
+ const from = update.message?.from;
151
+ if (from) {
152
+ return { userId: from.id, firstName: from.first_name };
153
+ }
154
+ }
155
+ return null;
156
+ }
157
+ catch {
158
+ return null;
159
+ }
160
+ finally {
161
+ clearTimeout(timeout);
162
+ }
163
+ }
133
164
  // ---------- Per-Channel Handlers ----------
134
165
  /**
135
166
  * Collect Discord bot token and optional guild IDs with live validation.
@@ -456,17 +487,63 @@ export const channelsStep = {
456
487
  message: "Grant admin access to specific senders?",
457
488
  });
458
489
  if (wantTrust) {
459
- prompter.note(info("Enter YOUR user ID (not the bot ID).\nTelegram: message @userinfobot to get yours.\nDiscord: enable Developer Mode → right-click your name → Copy User ID."));
460
- const input = await prompter.text({
461
- message: "Your sender IDs (comma-separated)",
462
- placeholder: "e.g. 678314278",
463
- validate: (v) => v.trim().length === 0 ? "At least one sender ID required" : undefined,
464
- });
465
- const entries = input
466
- .split(",")
467
- .map((s) => s.trim())
468
- .filter((s) => s.length > 0)
469
- .map((senderId) => ({ senderId, level: "admin" }));
490
+ const telegramConfig = configs.find((c) => c.type === "telegram");
491
+ let detectedId = null;
492
+ if (telegramConfig?.botToken) {
493
+ const wantDetect = await prompter.confirm({
494
+ message: "Auto-detect your Telegram user ID? (send any message to your bot first)",
495
+ });
496
+ if (wantDetect) {
497
+ const spin = prompter.spinner();
498
+ spin.start("Checking for messages...");
499
+ const sender = await fetchTelegramSenderId(telegramConfig.botToken);
500
+ if (sender) {
501
+ spin.stop(`Found: ${sender.firstName} (${sender.userId})`);
502
+ const useIt = await prompter.confirm({
503
+ message: `Use ${sender.userId} as your admin sender ID?`,
504
+ });
505
+ if (useIt) {
506
+ detectedId = String(sender.userId);
507
+ }
508
+ }
509
+ else {
510
+ spin.stop("No messages found");
511
+ prompter.log.warn("Send a message to your bot in Telegram and try again, or enter your ID manually below.");
512
+ }
513
+ }
514
+ }
515
+ const hasOtherChannels = configs.some((c) => c.type !== "telegram" && c.type !== "whatsapp" && c.type !== "signal" && c.type !== "irc");
516
+ let senderIds;
517
+ if (detectedId && !hasOtherChannels) {
518
+ senderIds = [detectedId];
519
+ }
520
+ else {
521
+ if (detectedId) {
522
+ prompter.note(info("Add sender IDs for your other channels.\nDiscord: enable Developer Mode → right-click your name → Copy User ID."));
523
+ }
524
+ else {
525
+ prompter.note(info("Enter YOUR user ID (not the bot ID).\nTelegram: send a message to your bot, then re-run setup to auto-detect — or find it via Telegram API.\nDiscord: enable Developer Mode → right-click your name → Copy User ID."));
526
+ }
527
+ const input = await prompter.text({
528
+ message: "Your sender IDs (comma-separated)",
529
+ placeholder: detectedId ? `e.g. ${detectedId}, <discord-id>` : "e.g. 678314278",
530
+ defaultValue: detectedId ? `${detectedId}, ` : "",
531
+ validate: (v) => v.trim().length === 0 ? "At least one sender ID required" : undefined,
532
+ });
533
+ senderIds = input
534
+ .split(",")
535
+ .map((s) => s.trim())
536
+ .filter((s) => s.length > 0);
537
+ }
538
+ const entries = senderIds.map((senderId) => ({ senderId, level: "admin" }));
539
+ if (telegramConfig && senderIds.length > 0) {
540
+ const restrictBot = await prompter.confirm({
541
+ message: "Restrict Telegram bot to only respond to these senders?",
542
+ });
543
+ if (restrictBot) {
544
+ telegramConfig.allowFrom = senderIds;
545
+ }
546
+ }
470
547
  return updateState(state, { channels: configs, senderTrustEntries: entries });
471
548
  }
472
549
  }
@@ -131,6 +131,10 @@ function buildConfigObject(state) {
131
131
  if (ch.type === "discord" && ch.guildIds && ch.guildIds.length > 0) {
132
132
  entry.guildIds = ch.guildIds;
133
133
  }
134
+ // Sender allowlist
135
+ if (ch.allowFrom && ch.allowFrom.length > 0) {
136
+ entry.allowFrom = ch.allowFrom;
137
+ }
134
138
  channels[ch.type] = entry;
135
139
  }
136
140
  config.channels = channels;
@@ -49,6 +49,7 @@ export type ChannelConfig = {
49
49
  appToken?: string;
50
50
  channelSecret?: string;
51
51
  guildIds?: string[];
52
+ allowFrom?: string[];
52
53
  validated?: boolean;
53
54
  };
54
55
  /** Per-tool-provider collected credentials. */
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@comis/cli",
3
3
  "private": true,
4
- "version": "1.0.9",
4
+ "version": "1.0.11",
5
5
  "author": "Moshe Anconina",
6
6
  "license": "Apache-2.0",
7
7
  "description": "Command-line interface for the Comis AI agent platform",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@comis/core",
3
3
  "private": true,
4
- "version": "1.0.9",
4
+ "version": "1.0.11",
5
5
  "author": "Moshe Anconina",
6
6
  "license": "Apache-2.0",
7
7
  "description": "Core domain types, ports, event bus, security, and config for Comis",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@comis/daemon",
3
3
  "private": true,
4
- "version": "1.0.9",
4
+ "version": "1.0.11",
5
5
  "author": "Moshe Anconina",
6
6
  "license": "Apache-2.0",
7
7
  "description": "Background daemon and orchestrator for the Comis platform",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@comis/gateway",
3
3
  "private": true,
4
- "version": "1.0.9",
4
+ "version": "1.0.11",
5
5
  "author": "Moshe Anconina",
6
6
  "license": "Apache-2.0",
7
7
  "description": "HTTP, JSON-RPC, and WebSocket gateway for Comis",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@comis/infra",
3
3
  "private": true,
4
- "version": "1.0.9",
4
+ "version": "1.0.11",
5
5
  "author": "Moshe Anconina",
6
6
  "license": "Apache-2.0",
7
7
  "description": "Structured logging infrastructure for Comis",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@comis/memory",
3
3
  "private": true,
4
- "version": "1.0.9",
4
+ "version": "1.0.11",
5
5
  "author": "Moshe Anconina",
6
6
  "license": "Apache-2.0",
7
7
  "description": "SQLite memory, embeddings, and RAG storage for Comis agents",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@comis/scheduler",
3
3
  "private": true,
4
- "version": "1.0.9",
4
+ "version": "1.0.11",
5
5
  "author": "Moshe Anconina",
6
6
  "license": "Apache-2.0",
7
7
  "description": "Task scheduling and cron management for Comis",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@comis/shared",
3
3
  "private": true,
4
- "version": "1.0.9",
4
+ "version": "1.0.11",
5
5
  "author": "Moshe Anconina",
6
6
  "license": "Apache-2.0",
7
7
  "description": "Shared types and utilities for the Comis platform",
@@ -284,11 +284,11 @@ export const DANGEROUS_COMMAND_PATTERNS = [
284
284
  { pattern: /\beval\s/, reason: "Shell eval executes arbitrary code" },
285
285
  {
286
286
  pattern: /\bsource\s/,
287
- reason: "Shell source executes arbitrary script file",
287
+ reason: "Shell source executes arbitrary script file. For virtualenvs, call the venv binary directly (e.g. .venv/bin/python3, .venv/bin/pip) instead of sourcing activate",
288
288
  },
289
289
  {
290
290
  pattern: /^\.\s+\//,
291
- reason: "POSIX source (.) executes arbitrary script file",
291
+ reason: "POSIX source (.) executes arbitrary script file. For virtualenvs, call the venv binary directly (e.g. .venv/bin/python3, .venv/bin/pip) instead of sourcing activate",
292
292
  },
293
293
  // Category H -- Indirect command execution
294
294
  {
@@ -366,7 +366,7 @@ export function createExecTool(workspacePath, registry, secretManager, platformS
366
366
  }
367
367
  // Detect --break-system-packages for post-execution warning
368
368
  const breakSystemWarning = command.includes("--break-system-packages")
369
- ? "\u26a0\ufe0f WARNING: --break-system-packages modifies the system Python. Use a virtualenv instead: python3 -m venv .venv && source .venv/bin/activate && pip install ...\n\n"
369
+ ? "\u26a0\ufe0f WARNING: --break-system-packages modifies the system Python. Use a virtualenv instead: python3 -m venv .venv && .venv/bin/pip install ...\n\n"
370
370
  : "";
371
371
  // Log command start (truncate command to 200 chars for security)
372
372
  logger?.debug({ toolName: "exec", command: command.slice(0, 200), background, pty, ...(description && { description }) }, "Exec command start");
@@ -58,5 +58,5 @@ export async function processAudioAttachment(att, deps, buildHint) {
58
58
  deps.logger.warn({ url: att.url, error: String(e), hint: "Unexpected STT error; voice message will not be transcribed", errorKind: "internal" }, "Transcription threw unexpectedly");
59
59
  deps.logger.debug?.({ url: att.url, reason: "stt-failed", err: String(e) }, "Transcription threw unexpectedly");
60
60
  }
61
- return {};
61
+ return { textPrefix: "[Voice message received but transcription failed — ask the user to send a text message instead]" };
62
62
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@comis/skills",
3
3
  "private": true,
4
- "version": "1.0.9",
4
+ "version": "1.0.11",
5
5
  "author": "Moshe Anconina",
6
6
  "license": "Apache-2.0",
7
7
  "description": "Skill system, MCP integration, and tool sandbox for Comis agents",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "comisai",
3
- "version": "1.0.9",
3
+ "version": "1.0.11",
4
4
  "author": "Moshe Anconina",
5
5
  "license": "Apache-2.0",
6
6
  "description": "Security-first AI agent platform — connects AI agents to Discord, Telegram, Slack, WhatsApp, and more",
@@ -115,17 +115,17 @@
115
115
  "@comis/daemon"
116
116
  ],
117
117
  "dependencies": {
118
- "@comis/shared": "1.0.9",
119
- "@comis/core": "1.0.9",
120
- "@comis/infra": "1.0.9",
121
- "@comis/memory": "1.0.9",
122
- "@comis/gateway": "1.0.9",
123
- "@comis/skills": "1.0.9",
124
- "@comis/scheduler": "1.0.9",
125
- "@comis/agent": "1.0.9",
126
- "@comis/channels": "1.0.9",
127
- "@comis/cli": "1.0.9",
128
- "@comis/daemon": "1.0.9",
118
+ "@comis/shared": "1.0.11",
119
+ "@comis/core": "1.0.11",
120
+ "@comis/infra": "1.0.11",
121
+ "@comis/memory": "1.0.11",
122
+ "@comis/gateway": "1.0.11",
123
+ "@comis/skills": "1.0.11",
124
+ "@comis/scheduler": "1.0.11",
125
+ "@comis/agent": "1.0.11",
126
+ "@comis/channels": "1.0.11",
127
+ "@comis/cli": "1.0.11",
128
+ "@comis/daemon": "1.0.11",
129
129
  "@agentclientprotocol/sdk": "^0.15.0",
130
130
  "@clack/core": "^1.1.0",
131
131
  "@clack/prompts": "^1.1.0",