assistme 0.3.3 → 0.3.4

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/dist/index.js CHANGED
@@ -171,22 +171,36 @@ async function getConversationHistory(conversationId, excludeMessageId, limit =
171
171
  }
172
172
 
173
173
  // src/db/event.ts
174
+ var MAX_EMIT_RETRIES = 2;
175
+ var EMIT_RETRY_DELAY_MS = 500;
176
+ async function emitWithRetry(messageId, eventType, eventData, seq) {
177
+ for (let attempt = 0; attempt <= MAX_EMIT_RETRIES; attempt++) {
178
+ try {
179
+ await callMcpHandler("event.emit", {
180
+ message_id: messageId,
181
+ event_type: eventType,
182
+ event_data: eventData,
183
+ seq
184
+ });
185
+ return;
186
+ } catch (err) {
187
+ if (attempt < MAX_EMIT_RETRIES) {
188
+ await new Promise((r) => setTimeout(r, EMIT_RETRY_DELAY_MS * (attempt + 1)));
189
+ } else {
190
+ log.warn(
191
+ `Failed to emit event after ${MAX_EMIT_RETRIES + 1} attempts: ${err instanceof Error ? err.message : err}`
192
+ );
193
+ }
194
+ }
195
+ }
196
+ }
174
197
  var eventSequence = 0;
175
198
  function resetEventSequence() {
176
199
  eventSequence = 0;
177
200
  }
178
201
  async function emitEvent(messageId, eventType, eventData) {
179
202
  eventSequence++;
180
- try {
181
- await callMcpHandler("event.emit", {
182
- message_id: messageId,
183
- event_type: eventType,
184
- event_data: eventData,
185
- seq: eventSequence
186
- });
187
- } catch (err) {
188
- log.warn(`Failed to emit event: ${err instanceof Error ? err.message : err}`);
189
- }
203
+ await emitWithRetry(messageId, eventType, eventData, eventSequence);
190
204
  }
191
205
 
192
206
  // src/db/action.ts
@@ -4183,7 +4197,7 @@ function getCredentialStore() {
4183
4197
 
4184
4198
  // src/mcp/agent-tools-server.ts
4185
4199
  function createAgentToolsServer(deps) {
4186
- const { memoryManager, skillManager, taskId, sessionId } = deps;
4200
+ const { memoryManager, skillManager, taskId, sessionId, onUserWaitStart, onUserWaitEnd } = deps;
4187
4201
  return createSdkMcpServer2({
4188
4202
  name: "assistme-agent",
4189
4203
  version: "1.0.0",
@@ -4688,52 +4702,56 @@ Use \`ask_user\` to request these from the user, or create them yourself (e.g. r
4688
4702
  try {
4689
4703
  await setActionRequest(taskId, actionData);
4690
4704
  log.info(`Ask user ${actionId}: "${args.question.slice(0, 80)}..."`);
4691
- emitEvent(taskId, "user_action_request", actionData).catch(() => {
4692
- });
4693
- emitEvent(taskId, "status_change", {
4705
+ await emitEvent(taskId, "user_action_request", actionData);
4706
+ await emitEvent(taskId, "status_change", {
4694
4707
  status: "waiting_for_user",
4695
4708
  message: args.question
4696
- }).catch(() => {
4697
4709
  });
4710
+ onUserWaitStart?.();
4698
4711
  const startTime = Date.now();
4699
4712
  const pollInterval = 2e3;
4700
- while (Date.now() - startTime < timeout) {
4701
- const response = await pollActionResponse(taskId);
4702
- if (response && (!response.action_id || response.action_id === actionId)) {
4703
- const actionKey = response.action_key || "";
4704
- const text = response.text || "";
4705
- const label = response.label || actionKey || text;
4706
- log.info(`User responded: "${label}"`);
4707
- return {
4708
- content: [
4709
- {
4710
- type: "text",
4711
- text: JSON.stringify({
4712
- status: "responded",
4713
- action_key: actionKey || "custom_input",
4714
- label,
4715
- text: text || label
4716
- })
4717
- }
4718
- ]
4719
- };
4713
+ try {
4714
+ while (Date.now() - startTime < timeout) {
4715
+ const response = await pollActionResponse(taskId);
4716
+ if (response && (!response.action_id || response.action_id === actionId)) {
4717
+ const actionKey = response.action_key || "";
4718
+ const text = response.text || "";
4719
+ const label = response.label || actionKey || text;
4720
+ log.info(`User responded: "${label}"`);
4721
+ return {
4722
+ content: [
4723
+ {
4724
+ type: "text",
4725
+ text: JSON.stringify({
4726
+ status: "responded",
4727
+ action_key: actionKey || "custom_input",
4728
+ label,
4729
+ text: text || label
4730
+ })
4731
+ }
4732
+ ]
4733
+ };
4734
+ }
4735
+ await new Promise((resolve2) => setTimeout(resolve2, pollInterval));
4720
4736
  }
4721
- await new Promise((resolve2) => setTimeout(resolve2, pollInterval));
4737
+ log.warn(`Ask user ${actionId} timed out after ${args.timeout_seconds || 300}s`);
4738
+ return {
4739
+ content: [
4740
+ {
4741
+ type: "text",
4742
+ text: JSON.stringify({
4743
+ status: "timeout",
4744
+ message: "User did not respond within the timeout period. Continue the task with a reasonable default or skip the step that required user input."
4745
+ })
4746
+ }
4747
+ ]
4748
+ };
4749
+ } finally {
4750
+ onUserWaitEnd?.();
4722
4751
  }
4723
- log.warn(`Ask user ${actionId} timed out after ${args.timeout_seconds || 300}s`);
4724
- return {
4725
- content: [
4726
- {
4727
- type: "text",
4728
- text: JSON.stringify({
4729
- status: "timeout",
4730
- message: "User did not respond within the timeout period."
4731
- })
4732
- }
4733
- ]
4734
- };
4735
4752
  } catch (err) {
4736
4753
  log.error(`ask_user failed: ${err}`);
4754
+ onUserWaitEnd?.();
4737
4755
  return {
4738
4756
  content: [
4739
4757
  {
@@ -5167,12 +5185,16 @@ Available capabilities:
5167
5185
  - Bash tool for shell commands
5168
5186
  - Glob and Grep for file search
5169
5187
 
5170
- 3. MEMORY:
5188
+ 3. MEMORY & CREDENTIALS:
5171
5189
  - You can remember things about the user using memory_store
5172
5190
  - Use this when you learn preferences, important facts, or standing instructions
5173
5191
  - Your stored memories persist across conversations
5174
5192
  - PROACTIVELY use memory_store during tasks when you discover user preferences, habits, or important context
5175
5193
  - Before completing a task, consider if anything learned should be remembered for future conversations
5194
+ - CRITICAL \u2014 Credential Storage: When you create, register, or receive any account credentials (username, password, API keys, tokens), you MUST use credential_set to save them locally. NEVER use memory_store for credentials \u2014 memory_store is for preferences and facts, credential_set is for secrets. Examples:
5195
+ * After registering a new email/account \u2192 credential_set with type "login" and data { "username": "...", "password": "...", "email": "..." }
5196
+ * After generating an API key \u2192 credential_set with type "api_key" and data { "api_key": "..." }
5197
+ * Credentials saved via credential_set are encrypted on disk and viewable in the desktop app's Credentials panel
5176
5198
 
5177
5199
  4. SKILL-AWARE EXECUTION (CRITICAL \u2014 follow this for EVERY task):
5178
5200
  Step A \u2014 Search: Before executing ANY task, check if an existing skill matches (use skill_invoke or skill_search).
@@ -5252,6 +5274,42 @@ CRITICAL \u2014 Ask before you guess:
5252
5274
  Workspace path: {workspace_path}`;
5253
5275
 
5254
5276
  // src/agent/processor.ts
5277
+ var TaskTimeout = class {
5278
+ constructor(abortController, timeoutMs) {
5279
+ this.abortController = abortController;
5280
+ this.remainingMs = timeoutMs;
5281
+ this.resumedAt = Date.now();
5282
+ this.schedule();
5283
+ }
5284
+ timeoutId = null;
5285
+ remainingMs;
5286
+ resumedAt;
5287
+ schedule() {
5288
+ this.timeoutId = setTimeout(() => {
5289
+ this.abortController.abort();
5290
+ }, this.remainingMs);
5291
+ }
5292
+ /** Pause the timeout (e.g. while waiting for user). */
5293
+ pause() {
5294
+ if (this.timeoutId) {
5295
+ clearTimeout(this.timeoutId);
5296
+ this.timeoutId = null;
5297
+ const elapsed = Date.now() - this.resumedAt;
5298
+ this.remainingMs = Math.max(0, this.remainingMs - elapsed);
5299
+ }
5300
+ }
5301
+ /** Resume the timeout after user interaction completes. */
5302
+ resume() {
5303
+ this.resumedAt = Date.now();
5304
+ this.schedule();
5305
+ }
5306
+ clear() {
5307
+ if (this.timeoutId) {
5308
+ clearTimeout(this.timeoutId);
5309
+ this.timeoutId = null;
5310
+ }
5311
+ }
5312
+ };
5255
5313
  var MAX_HISTORY_ENTRIES = 10;
5256
5314
  var MAX_RESPONSE_LENGTH = 1500;
5257
5315
  var TaskProcessor = class {
@@ -5334,12 +5392,16 @@ var TaskProcessor = class {
5334
5392
  }
5335
5393
  systemPrompt += historyPrompt;
5336
5394
  }
5395
+ const abortController = new AbortController();
5396
+ const taskTimeout = new TaskTimeout(abortController, taskTimeoutMs);
5337
5397
  const browserServer = createBrowserMcpServer();
5338
5398
  const agentToolsServer = createAgentToolsServer({
5339
5399
  memoryManager: this.memoryManager,
5340
5400
  skillManager: this.skillManager,
5341
5401
  taskId: task.id,
5342
- sessionId: this.sessionId || void 0
5402
+ sessionId: this.sessionId || void 0,
5403
+ onUserWaitStart: () => taskTimeout.pause(),
5404
+ onUserWaitEnd: () => taskTimeout.resume()
5343
5405
  });
5344
5406
  const eventHooks = createEventHooks(task.id, toolCallRecords);
5345
5407
  const allowedTools = [
@@ -5386,7 +5448,6 @@ var TaskProcessor = class {
5386
5448
  session_id: ""
5387
5449
  };
5388
5450
  }
5389
- const abortController = new AbortController();
5390
5451
  const options = {
5391
5452
  model: config.model,
5392
5453
  systemPrompt,
@@ -5404,9 +5465,6 @@ var TaskProcessor = class {
5404
5465
  abortController
5405
5466
  };
5406
5467
  const taskStartTime = Date.now();
5407
- const timeoutId = setTimeout(() => {
5408
- abortController.abort();
5409
- }, taskTimeoutMs);
5410
5468
  try {
5411
5469
  for await (const message of query2({
5412
5470
  prompt: promptMessages(),
@@ -5468,7 +5526,7 @@ var TaskProcessor = class {
5468
5526
  }
5469
5527
  }
5470
5528
  } finally {
5471
- clearTimeout(timeoutId);
5529
+ taskTimeout.clear();
5472
5530
  }
5473
5531
  const MAX_CONTENT_LENGTH = 5e4;
5474
5532
  const truncatedResponse = finalResponse.length > MAX_CONTENT_LENGTH ? finalResponse.slice(0, MAX_CONTENT_LENGTH) + "\n\n[Response truncated]" : finalResponse;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "assistme",
3
- "version": "0.3.3",
3
+ "version": "0.3.4",
4
4
  "description": "AssistMe CLI Agent - AI-powered assistant that controls your real browser",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -31,6 +31,55 @@ import {
31
31
  import { createEventHooks } from "./event-hooks.js";
32
32
  import { BASE_SYSTEM_PROMPT } from "./system-prompt.js";
33
33
 
34
+ /**
35
+ * Manages the task wall-clock timeout.
36
+ * Supports pausing while the agent is waiting for user input (ask_user)
37
+ * so that idle wait time doesn't count toward the timeout.
38
+ */
39
+ class TaskTimeout {
40
+ private timeoutId: ReturnType<typeof setTimeout> | null = null;
41
+ private remainingMs: number;
42
+ private resumedAt: number;
43
+
44
+ constructor(
45
+ private abortController: AbortController,
46
+ timeoutMs: number
47
+ ) {
48
+ this.remainingMs = timeoutMs;
49
+ this.resumedAt = Date.now();
50
+ this.schedule();
51
+ }
52
+
53
+ private schedule(): void {
54
+ this.timeoutId = setTimeout(() => {
55
+ this.abortController.abort();
56
+ }, this.remainingMs);
57
+ }
58
+
59
+ /** Pause the timeout (e.g. while waiting for user). */
60
+ pause(): void {
61
+ if (this.timeoutId) {
62
+ clearTimeout(this.timeoutId);
63
+ this.timeoutId = null;
64
+ const elapsed = Date.now() - this.resumedAt;
65
+ this.remainingMs = Math.max(0, this.remainingMs - elapsed);
66
+ }
67
+ }
68
+
69
+ /** Resume the timeout after user interaction completes. */
70
+ resume(): void {
71
+ this.resumedAt = Date.now();
72
+ this.schedule();
73
+ }
74
+
75
+ clear(): void {
76
+ if (this.timeoutId) {
77
+ clearTimeout(this.timeoutId);
78
+ this.timeoutId = null;
79
+ }
80
+ }
81
+ }
82
+
34
83
  const MAX_HISTORY_ENTRIES = 10;
35
84
  const MAX_RESPONSE_LENGTH = 1500;
36
85
 
@@ -143,6 +192,9 @@ export class TaskProcessor {
143
192
  systemPrompt += historyPrompt;
144
193
  }
145
194
 
195
+ const abortController = new AbortController();
196
+ const taskTimeout = new TaskTimeout(abortController, taskTimeoutMs);
197
+
146
198
  // Create MCP servers for custom tools
147
199
  const browserServer = createBrowserMcpServer();
148
200
  const agentToolsServer = createAgentToolsServer({
@@ -150,6 +202,8 @@ export class TaskProcessor {
150
202
  skillManager: this.skillManager,
151
203
  taskId: task.id,
152
204
  sessionId: this.sessionId || undefined,
205
+ onUserWaitStart: () => taskTimeout.pause(),
206
+ onUserWaitEnd: () => taskTimeout.resume(),
153
207
  });
154
208
 
155
209
  // Create event hooks for Supabase event emission
@@ -203,7 +257,6 @@ export class TaskProcessor {
203
257
  };
204
258
  }
205
259
 
206
- const abortController = new AbortController();
207
260
  const options: Options = {
208
261
  model: config.model,
209
262
  systemPrompt,
@@ -221,11 +274,7 @@ export class TaskProcessor {
221
274
  abortController,
222
275
  };
223
276
 
224
- // Wall-clock timeout via abort
225
277
  const taskStartTime = Date.now();
226
- const timeoutId = setTimeout(() => {
227
- abortController.abort();
228
- }, taskTimeoutMs);
229
278
 
230
279
  try {
231
280
  for await (const message of query({
@@ -302,7 +351,7 @@ export class TaskProcessor {
302
351
  }
303
352
  }
304
353
  } finally {
305
- clearTimeout(timeoutId);
354
+ taskTimeout.clear();
306
355
  }
307
356
 
308
357
  // Truncate finalResponse to avoid edge function payload limits
@@ -41,12 +41,16 @@ Available capabilities:
41
41
  - Bash tool for shell commands
42
42
  - Glob and Grep for file search
43
43
 
44
- 3. MEMORY:
44
+ 3. MEMORY & CREDENTIALS:
45
45
  - You can remember things about the user using memory_store
46
46
  - Use this when you learn preferences, important facts, or standing instructions
47
47
  - Your stored memories persist across conversations
48
48
  - PROACTIVELY use memory_store during tasks when you discover user preferences, habits, or important context
49
49
  - Before completing a task, consider if anything learned should be remembered for future conversations
50
+ - CRITICAL — Credential Storage: When you create, register, or receive any account credentials (username, password, API keys, tokens), you MUST use credential_set to save them locally. NEVER use memory_store for credentials — memory_store is for preferences and facts, credential_set is for secrets. Examples:
51
+ * After registering a new email/account → credential_set with type "login" and data { "username": "...", "password": "...", "email": "..." }
52
+ * After generating an API key → credential_set with type "api_key" and data { "api_key": "..." }
53
+ * Credentials saved via credential_set are encrypted on disk and viewable in the desktop app's Credentials panel
50
54
 
51
55
  4. SKILL-AWARE EXECUTION (CRITICAL — follow this for EVERY task):
52
56
  Step A — Search: Before executing ANY task, check if an existing skill matches (use skill_invoke or skill_search).
package/src/db/event.ts CHANGED
@@ -2,6 +2,36 @@ import { callMcpHandler } from "./api-client.js";
2
2
  import { log } from "../utils/logger.js";
3
3
  import type { EventType } from "./types.js";
4
4
 
5
+ const MAX_EMIT_RETRIES = 2;
6
+ const EMIT_RETRY_DELAY_MS = 500;
7
+
8
+ async function emitWithRetry(
9
+ messageId: string,
10
+ eventType: EventType,
11
+ eventData: Record<string, unknown>,
12
+ seq: number
13
+ ): Promise<void> {
14
+ for (let attempt = 0; attempt <= MAX_EMIT_RETRIES; attempt++) {
15
+ try {
16
+ await callMcpHandler("event.emit", {
17
+ message_id: messageId,
18
+ event_type: eventType,
19
+ event_data: eventData,
20
+ seq,
21
+ });
22
+ return;
23
+ } catch (err) {
24
+ if (attempt < MAX_EMIT_RETRIES) {
25
+ await new Promise((r) => setTimeout(r, EMIT_RETRY_DELAY_MS * (attempt + 1)));
26
+ } else {
27
+ log.warn(
28
+ `Failed to emit event after ${MAX_EMIT_RETRIES + 1} attempts: ${err instanceof Error ? err.message : err}`
29
+ );
30
+ }
31
+ }
32
+ }
33
+ }
34
+
5
35
  /**
6
36
  * Per-task event emitter. Each task gets its own sequence counter
7
37
  * to avoid cross-task sequence number collisions.
@@ -13,16 +43,7 @@ export class TaskEventEmitter {
13
43
 
14
44
  async emit(eventType: EventType, eventData: Record<string, unknown>): Promise<void> {
15
45
  this.sequence++;
16
- try {
17
- await callMcpHandler("event.emit", {
18
- message_id: this.messageId,
19
- event_type: eventType,
20
- event_data: eventData,
21
- seq: this.sequence,
22
- });
23
- } catch (err) {
24
- log.warn(`Failed to emit event: ${err instanceof Error ? err.message : err}`);
25
- }
46
+ await emitWithRetry(this.messageId, eventType, eventData, this.sequence);
26
47
  }
27
48
  }
28
49
 
@@ -39,14 +60,5 @@ export async function emitEvent(
39
60
  eventData: Record<string, unknown>
40
61
  ): Promise<void> {
41
62
  eventSequence++;
42
- try {
43
- await callMcpHandler("event.emit", {
44
- message_id: messageId,
45
- event_type: eventType,
46
- event_data: eventData,
47
- seq: eventSequence,
48
- });
49
- } catch (err) {
50
- log.warn(`Failed to emit event: ${err instanceof Error ? err.message : err}`);
51
- }
63
+ await emitWithRetry(messageId, eventType, eventData, eventSequence);
52
64
  }
@@ -25,10 +25,14 @@ export interface AgentToolsDeps {
25
25
  skillManager: SkillManager;
26
26
  taskId: string;
27
27
  sessionId?: string;
28
+ /** Called when the agent starts waiting for user input (pauses task timeout). */
29
+ onUserWaitStart?: () => void;
30
+ /** Called when user input completes or times out (resumes task timeout). */
31
+ onUserWaitEnd?: () => void;
28
32
  }
29
33
 
30
34
  export function createAgentToolsServer(deps: AgentToolsDeps): McpSdkServerConfigWithInstance {
31
- const { memoryManager, skillManager, taskId, sessionId } = deps;
35
+ const { memoryManager, skillManager, taskId, sessionId, onUserWaitStart, onUserWaitEnd } = deps;
32
36
 
33
37
  return createSdkMcpServer({
34
38
  name: "assistme-agent",
@@ -582,56 +586,65 @@ export function createAgentToolsServer(deps: AgentToolsDeps): McpSdkServerConfig
582
586
  await setActionRequest(taskId, actionData);
583
587
  log.info(`Ask user ${actionId}: "${args.question.slice(0, 80)}..."`);
584
588
 
585
- emitEvent(taskId, "user_action_request", actionData).catch(() => {});
586
- // Emit waiting_for_user status so the web UI can show a waiting indicator
587
- emitEvent(taskId, "status_change", {
589
+ // Await event emissions to ensure they reach the DB before we start polling
590
+ await emitEvent(taskId, "user_action_request", actionData);
591
+ await emitEvent(taskId, "status_change", {
588
592
  status: "waiting_for_user",
589
593
  message: args.question,
590
- }).catch(() => {});
594
+ });
595
+
596
+ // Pause the task wall-clock timeout while waiting for user
597
+ onUserWaitStart?.();
591
598
 
592
599
  const startTime = Date.now();
593
600
  const pollInterval = 2000;
594
601
 
595
- while (Date.now() - startTime < timeout) {
596
- const response = await pollActionResponse(taskId);
597
- if (response && (!response.action_id || response.action_id === actionId)) {
598
- // Response can be either an option click or free-text input
599
- const actionKey = (response.action_key || "") as string;
600
- const text = (response.text || "") as string;
601
- const label = (response.label || actionKey || text) as string;
602
- log.info(`User responded: "${label}"`);
603
- return {
604
- content: [
605
- {
606
- type: "text",
607
- text: JSON.stringify({
608
- status: "responded",
609
- action_key: actionKey || "custom_input",
610
- label,
611
- text: text || label,
612
- }),
613
- },
614
- ],
615
- };
602
+ try {
603
+ while (Date.now() - startTime < timeout) {
604
+ const response = await pollActionResponse(taskId);
605
+ if (response && (!response.action_id || response.action_id === actionId)) {
606
+ const actionKey = (response.action_key || "") as string;
607
+ const text = (response.text || "") as string;
608
+ const label = (response.label || actionKey || text) as string;
609
+ log.info(`User responded: "${label}"`);
610
+ return {
611
+ content: [
612
+ {
613
+ type: "text",
614
+ text: JSON.stringify({
615
+ status: "responded",
616
+ action_key: actionKey || "custom_input",
617
+ label,
618
+ text: text || label,
619
+ }),
620
+ },
621
+ ],
622
+ };
623
+ }
624
+
625
+ await new Promise((resolve) => setTimeout(resolve, pollInterval));
616
626
  }
617
627
 
618
- await new Promise((resolve) => setTimeout(resolve, pollInterval));
628
+ log.warn(`Ask user ${actionId} timed out after ${args.timeout_seconds || 300}s`);
629
+ return {
630
+ content: [
631
+ {
632
+ type: "text",
633
+ text: JSON.stringify({
634
+ status: "timeout",
635
+ message:
636
+ "User did not respond within the timeout period. Continue the task with a reasonable default or skip the step that required user input.",
637
+ }),
638
+ },
639
+ ],
640
+ };
641
+ } finally {
642
+ // Resume the task timeout regardless of outcome
643
+ onUserWaitEnd?.();
619
644
  }
620
-
621
- log.warn(`Ask user ${actionId} timed out after ${args.timeout_seconds || 300}s`);
622
- return {
623
- content: [
624
- {
625
- type: "text",
626
- text: JSON.stringify({
627
- status: "timeout",
628
- message: "User did not respond within the timeout period.",
629
- }),
630
- },
631
- ],
632
- };
633
645
  } catch (err) {
634
646
  log.error(`ask_user failed: ${err}`);
647
+ onUserWaitEnd?.();
635
648
  return {
636
649
  content: [
637
650
  {