assistme 0.2.4 → 0.2.5

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.
@@ -340,6 +340,24 @@ async function emitEvent(messageId, eventType, eventData) {
340
340
  });
341
341
  if (error) log.warn(`Failed to emit event: ${error.message}`);
342
342
  }
343
+ async function setActionRequest(messageId, actionData) {
344
+ const sb = getSupabase();
345
+ const { error } = await sb.rpc("mcp_set_action_request", {
346
+ p_token_hash: getTokenHash(),
347
+ p_message_id: messageId,
348
+ p_action_data: actionData
349
+ });
350
+ if (error) throw new Error(`Failed to set action request: ${error.message}`);
351
+ }
352
+ async function pollActionResponse(messageId) {
353
+ const sb = getSupabase();
354
+ const { data, error } = await sb.rpc("mcp_poll_action_response", {
355
+ p_token_hash: getTokenHash(),
356
+ p_message_id: messageId
357
+ });
358
+ if (error) throw new Error(`Failed to poll action response: ${error.message}`);
359
+ return data;
360
+ }
343
361
 
344
362
  // src/agent/job-runner.ts
345
363
  var JobRunner = class {
@@ -556,5 +574,7 @@ export {
556
574
  getConversationHistory,
557
575
  resetEventSequence,
558
576
  emitEvent,
577
+ setActionRequest,
578
+ pollActionResponse,
559
579
  JobRunner
560
580
  };
package/dist/index.js CHANGED
@@ -19,15 +19,17 @@ import {
19
19
  loginWithToken,
20
20
  logout,
21
21
  newCorrelationId,
22
+ pollActionResponse,
22
23
  pollAndClaimJobRun,
23
24
  pollAndClaimTask,
24
25
  resetEventSequence,
26
+ setActionRequest,
25
27
  setConfig,
26
28
  setCorrelationId,
27
29
  setLogLevel,
28
30
  setSessionBusy,
29
31
  updateHeartbeat
30
- } from "./chunk-GHMZQ2UA.js";
32
+ } from "./chunk-XY3LGAOY.js";
31
33
 
32
34
  // src/index.ts
33
35
  import { Command } from "commander";
@@ -2974,7 +2976,7 @@ function createAgentToolsServer(deps) {
2974
2976
  ),
2975
2977
  tool(
2976
2978
  "skill_create",
2977
- "Create a new skill definition. The skill is saved as a draft \u2014 you must then ask the user if they want to add it to their collection, and use skill_add if they approve.",
2979
+ "Create a new skill and add it to the user's collection. Returns the skill ID on success.",
2978
2980
  {
2979
2981
  name: z.string().describe("Skill name in kebab-case, e.g. 'flight-booking'"),
2980
2982
  description: z.string().describe("One-line description of what this skill does"),
@@ -3004,12 +3006,23 @@ function createAgentToolsServer(deps) {
3004
3006
  content: [{ type: "text", text: `Failed to create skill "${args.name}".` }]
3005
3007
  };
3006
3008
  }
3007
- log.success(`Skill "${args.name}" created (pending approval)`);
3009
+ await skillManager.syncToAgentSkills(
3010
+ args.name,
3011
+ args.description,
3012
+ args.instructions,
3013
+ "1.0.0",
3014
+ {
3015
+ source: "manual",
3016
+ emoji: args.emoji,
3017
+ sourceSkillId: result.id
3018
+ }
3019
+ );
3020
+ log.success(`Skill "${args.name}" created and added to collection`);
3008
3021
  return {
3009
3022
  content: [
3010
3023
  {
3011
3024
  type: "text",
3012
- text: `Skill "${args.name}" created (ID: ${result.id}). Ask the user if they want to add it to their collection. If yes, call skill_add with this ID.`
3025
+ text: `Skill "${args.name}" created and added to your collection (ID: ${result.id}).`
3013
3026
  }
3014
3027
  ]
3015
3028
  };
@@ -3142,7 +3155,7 @@ ${content}`;
3142
3155
  ),
3143
3156
  tool(
3144
3157
  "skill_generate",
3145
- "Prepare context for generating skills from a job description. Returns existing skills and job info so you can analyze the job and create skills using skill_create + skill_add. After creating all skills, call skill_link_job to link them to the job and mark it as analyzed.",
3158
+ "Prepare context for generating skills from a job description. Returns existing skills and job info so you can analyze the job and create skills using skill_create (which auto-adds to user's collection). After creating all skills, call skill_link_job to link them to the job and mark it as analyzed.",
3146
3159
  {
3147
3160
  job_name: z.string().describe(
3148
3161
  "Short name for this job/role. Example: '\u7535\u5546\u8FD0\u8425', 'Frontend Dev', 'Data Analyst'"
@@ -3163,7 +3176,24 @@ ${content}`;
3163
3176
 
3164
3177
  `;
3165
3178
  }
3166
- response += `**Your task:** Analyze this job description and decompose it into 4-10 automatable skills. `;
3179
+ response += `**Your task:** Analyze this job description and decompose it into 4-10 automatable skills.
3180
+
3181
+ `;
3182
+ response += `**IMPORTANT \u2014 Confirmation workflow:**
3183
+ `;
3184
+ response += `1. First, analyze the job and draft a list of proposed skills (name, emoji, one-line description for each).
3185
+ `;
3186
+ response += `2. Then call \`request_user_confirmation\` to present these skills to the user for approval BEFORE creating any.
3187
+ `;
3188
+ response += ` - message: show a formatted list of all proposed skills
3189
+ `;
3190
+ response += ` - options: [{label: "Approve All", value: "approve_all"}, {label: "Cancel", value: "cancel"}]
3191
+ `;
3192
+ response += `3. If the user approves, create all skills using \`skill_create\` for each.
3193
+ `;
3194
+ response += `4. If the user cancels, stop without creating anything.
3195
+
3196
+ `;
3167
3197
  response += `For each skill, call \`skill_create\` with:
3168
3198
  `;
3169
3199
  response += `- name: kebab-case name (e.g. "slack-message-check")
@@ -3175,10 +3205,10 @@ ${content}`;
3175
3205
  response += `- emoji: a single emoji representing the skill
3176
3206
 
3177
3207
  `;
3178
- response += `After creating each skill, call \`skill_add\` with the returned skill ID to add it to the user's collection.
3208
+ response += `skill_create automatically adds the skill to the user's collection \u2014 no need to call skill_add.
3179
3209
 
3180
3210
  `;
3181
- response += `After ALL skills are created and added, call \`skill_link_job\` with job_name="${args.job_name}" and the list of created skill names to link them and mark the job as analyzed.
3211
+ response += `After ALL skills are created, call \`skill_link_job\` with job_name="${args.job_name}" and the list of created skill names to link them and mark the job as analyzed.
3182
3212
 
3183
3213
  `;
3184
3214
  response += `**Guidelines for skill instructions:**
@@ -3328,6 +3358,71 @@ ${content}`;
3328
3358
  };
3329
3359
  }
3330
3360
  ),
3361
+ // ── User Confirmation Tool ─────────────────────────────────
3362
+ tool(
3363
+ "request_user_confirmation",
3364
+ "Pause and ask the user for approval or input via the web UI. Returns the user's response. Use this before creating skills, making irreversible changes, etc.",
3365
+ {
3366
+ message: z.string().describe("What to show the user (supports markdown)"),
3367
+ options: z.array(z.object({
3368
+ label: z.string(),
3369
+ value: z.string(),
3370
+ description: z.string().optional()
3371
+ })).describe("Buttons/options to show the user"),
3372
+ timeout_seconds: z.number().optional().describe("How long to wait for response (default: 300)")
3373
+ },
3374
+ async (args) => {
3375
+ const actionId = crypto.randomUUID();
3376
+ const timeout = (args.timeout_seconds || 300) * 1e3;
3377
+ const actionData = {
3378
+ id: actionId,
3379
+ type: "confirmation",
3380
+ message: args.message,
3381
+ options: args.options,
3382
+ created_at: (/* @__PURE__ */ new Date()).toISOString()
3383
+ };
3384
+ try {
3385
+ await setActionRequest(taskId, actionData);
3386
+ await emitEvent(taskId, "user_action_request", actionData);
3387
+ const startTime = Date.now();
3388
+ const pollInterval = 2e3;
3389
+ while (Date.now() - startTime < timeout) {
3390
+ const response = await pollActionResponse(taskId);
3391
+ if (response) {
3392
+ log.info(`User responded to action request ${actionId}: ${JSON.stringify(response)}`);
3393
+ return {
3394
+ content: [{
3395
+ type: "text",
3396
+ text: JSON.stringify({
3397
+ status: "responded",
3398
+ action: response.action || response.value,
3399
+ response
3400
+ })
3401
+ }]
3402
+ };
3403
+ }
3404
+ await new Promise((resolve2) => setTimeout(resolve2, pollInterval));
3405
+ }
3406
+ log.warn(`Action request ${actionId} timed out after ${args.timeout_seconds || 300}s`);
3407
+ return {
3408
+ content: [{
3409
+ type: "text",
3410
+ text: JSON.stringify({
3411
+ status: "timeout",
3412
+ message: "User did not respond within the timeout period."
3413
+ })
3414
+ }]
3415
+ };
3416
+ } catch (err) {
3417
+ return {
3418
+ content: [{
3419
+ type: "text",
3420
+ text: `Failed to request user confirmation: ${err instanceof Error ? err.message : err}`
3421
+ }]
3422
+ };
3423
+ }
3424
+ }
3425
+ ),
3331
3426
  // ── Job Automation Tools ──────────────────────────────────────
3332
3427
  tool(
3333
3428
  "job_run",
@@ -3521,30 +3616,17 @@ ${content}`;
3521
3616
  async function saveJobToDb(userId, jobName, jobDescription, createdSkillNames) {
3522
3617
  try {
3523
3618
  const sb = getSupabase();
3524
- const { data: job, error: jobError } = await sb.from("agent_jobs").upsert(
3525
- { user_id: userId, name: jobName, description: jobDescription },
3526
- { onConflict: "user_id,name" }
3527
- ).select("id").single();
3528
- if (jobError || !job) {
3529
- log.debug(`Failed to save job "${jobName}": ${jobError?.message}`);
3619
+ const { data, error } = await sb.rpc("save_job_with_skills", {
3620
+ p_user_id: userId,
3621
+ p_job_name: jobName,
3622
+ p_job_description: jobDescription,
3623
+ p_skill_names: createdSkillNames
3624
+ });
3625
+ if (error) {
3626
+ log.debug(`Failed to save job "${jobName}" via RPC: ${error.message}`);
3530
3627
  return;
3531
3628
  }
3532
- if (createdSkillNames.length > 0) {
3533
- const { data: skillRows } = await sb.from("agent_skills").select("id").eq("user_id", userId).in("name", createdSkillNames);
3534
- const skillIds = (skillRows || []).map((r) => r.id);
3535
- if (skillIds.length > 0) {
3536
- const links = skillIds.map((skillId) => ({
3537
- job_id: job.id,
3538
- skill_id: skillId
3539
- }));
3540
- const { error: linkError } = await sb.from("agent_job_skills").upsert(links, { onConflict: "job_id,skill_id" });
3541
- if (linkError) {
3542
- log.debug(`Failed to link skills to job: ${linkError.message}`);
3543
- }
3544
- }
3545
- }
3546
- await sb.from("agent_jobs").update({ skills_analyzed: true }).eq("id", job.id);
3547
- log.debug(`Job "${jobName}" saved to DB with ${createdSkillNames.length} skill link(s), marked as analyzed`);
3629
+ log.debug(`Job "${jobName}" saved via RPC (id: ${data}), ${createdSkillNames.length} skill(s) linked`);
3548
3630
  } catch (err) {
3549
3631
  log.debug(`saveJobToDb error: ${err}`);
3550
3632
  }
@@ -3774,6 +3856,8 @@ var TaskProcessor = class {
3774
3856
  "mcp__assistme-agent__skill_browse",
3775
3857
  "mcp__assistme-agent__skill_add",
3776
3858
  "mcp__assistme-agent__skill_publish",
3859
+ // User confirmation
3860
+ "mcp__assistme-agent__request_user_confirmation",
3777
3861
  // Job automation tools
3778
3862
  "mcp__assistme-agent__job_run",
3779
3863
  "mcp__assistme-agent__job_schedule",
@@ -4446,7 +4530,7 @@ function registerJobCommands(program2) {
4446
4530
  jobCmd.command("list").description("List your defined jobs").action(async () => {
4447
4531
  try {
4448
4532
  const userId = await getCurrentUserId();
4449
- const { JobRunner: JobRunner2 } = await import("./job-runner-BH5MDQX3.js");
4533
+ const { JobRunner: JobRunner2 } = await import("./job-runner-XTGLMPZ3.js");
4450
4534
  const runner = new JobRunner2(userId);
4451
4535
  const jobs = await runner.listJobs();
4452
4536
  if (jobs.length === 0) {
@@ -4474,7 +4558,7 @@ function registerJobCommands(program2) {
4474
4558
  jobCmd.command("status [name]").description("Show run history for a job (or all jobs)").option("-l, --limit <number>", "Max runs to show (default: 5)").action(async (name, opts) => {
4475
4559
  try {
4476
4560
  const userId = await getCurrentUserId();
4477
- const { JobRunner: JobRunner2 } = await import("./job-runner-BH5MDQX3.js");
4561
+ const { JobRunner: JobRunner2 } = await import("./job-runner-XTGLMPZ3.js");
4478
4562
  const runner = new JobRunner2(userId);
4479
4563
  const runs = await runner.getRunHistory(
4480
4564
  name,
@@ -4528,7 +4612,7 @@ Job Run History${name ? ` \u2014 ${name}` : ""}:`
4528
4612
  process.exit(1);
4529
4613
  }
4530
4614
  const userId = await getCurrentUserId();
4531
- const { JobRunner: JobRunner2 } = await import("./job-runner-BH5MDQX3.js");
4615
+ const { JobRunner: JobRunner2 } = await import("./job-runner-XTGLMPZ3.js");
4532
4616
  const runner = new JobRunner2(userId);
4533
4617
  const job = await runner.loadJob(name);
4534
4618
  if (!job) {
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  JobRunner
3
- } from "./chunk-GHMZQ2UA.js";
3
+ } from "./chunk-XY3LGAOY.js";
4
4
  export {
5
5
  JobRunner
6
6
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "assistme",
3
- "version": "0.2.4",
3
+ "version": "0.2.5",
4
4
  "description": "AssistMe CLI Agent - AI-powered assistant that controls your real browser",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -10,7 +10,7 @@ import { log } from "../utils/logger.js";
10
10
  import type { MemoryManager, MemoryCategory } from "./memory.js";
11
11
  import type { SkillManager } from "./skills.js";
12
12
  import { substituteArguments, preprocessDynamicContext } from "./skills.js";
13
- import { getSupabase } from "../db/supabase.js";
13
+ import { getSupabase, emitEvent, setActionRequest, pollActionResponse } from "../db/supabase.js";
14
14
  import { JobRunner } from "./job-runner.js";
15
15
  import {
16
16
  createScheduledTask,
@@ -214,7 +214,7 @@ export function createAgentToolsServer(deps: AgentToolsDeps): McpSdkServerConfig
214
214
  ),
215
215
  tool(
216
216
  "skill_create",
217
- "Create a new skill definition. The skill is saved as a draft — you must then ask the user if they want to add it to their collection, and use skill_add if they approve.",
217
+ "Create a new skill and add it to the user's collection. Returns the skill ID on success.",
218
218
  {
219
219
  name: z.string().describe("Skill name in kebab-case, e.g. 'flight-booking'"),
220
220
  description: z.string().describe("One-line description of what this skill does"),
@@ -235,6 +235,7 @@ export function createAgentToolsServer(deps: AgentToolsDeps): McpSdkServerConfig
235
235
  };
236
236
  }
237
237
 
238
+ // Create in skills table (repository)
238
239
  const result = await skillManager.create(
239
240
  args.name,
240
241
  args.description,
@@ -248,12 +249,25 @@ export function createAgentToolsServer(deps: AgentToolsDeps): McpSdkServerConfig
248
249
  };
249
250
  }
250
251
 
251
- log.success(`Skill "${args.name}" created (pending approval)`);
252
+ // Auto-add to user's collection via upsert_agent_skill RPC
253
+ await skillManager.syncToAgentSkills(
254
+ args.name,
255
+ args.description,
256
+ args.instructions,
257
+ "1.0.0",
258
+ {
259
+ source: "manual",
260
+ emoji: args.emoji,
261
+ sourceSkillId: result.id,
262
+ }
263
+ );
264
+
265
+ log.success(`Skill "${args.name}" created and added to collection`);
252
266
  return {
253
267
  content: [
254
268
  {
255
269
  type: "text",
256
- text: `Skill "${args.name}" created (ID: ${result.id}). Ask the user if they want to add it to their collection. If yes, call skill_add with this ID.`,
270
+ text: `Skill "${args.name}" created and added to your collection (ID: ${result.id}).`,
257
271
  },
258
272
  ],
259
273
  };
@@ -405,7 +419,7 @@ export function createAgentToolsServer(deps: AgentToolsDeps): McpSdkServerConfig
405
419
  tool(
406
420
  "skill_generate",
407
421
  "Prepare context for generating skills from a job description. Returns existing skills and job info " +
408
- "so you can analyze the job and create skills using skill_create + skill_add. " +
422
+ "so you can analyze the job and create skills using skill_create (which auto-adds to user's collection). " +
409
423
  "After creating all skills, call skill_link_job to link them to the job and mark it as analyzed.",
410
424
  {
411
425
  job_name: z.string().describe(
@@ -426,14 +440,21 @@ export function createAgentToolsServer(deps: AgentToolsDeps): McpSdkServerConfig
426
440
  response += `**Existing skills (do NOT duplicate):** ${existingNames.join(", ")}\n\n`;
427
441
  }
428
442
 
429
- response += `**Your task:** Analyze this job description and decompose it into 4-10 automatable skills. `;
443
+ response += `**Your task:** Analyze this job description and decompose it into 4-10 automatable skills.\n\n`;
444
+ response += `**IMPORTANT — Confirmation workflow:**\n`;
445
+ response += `1. First, analyze the job and draft a list of proposed skills (name, emoji, one-line description for each).\n`;
446
+ response += `2. Then call \`request_user_confirmation\` to present these skills to the user for approval BEFORE creating any.\n`;
447
+ response += ` - message: show a formatted list of all proposed skills\n`;
448
+ response += ` - options: [{label: "Approve All", value: "approve_all"}, {label: "Cancel", value: "cancel"}]\n`;
449
+ response += `3. If the user approves, create all skills using \`skill_create\` for each.\n`;
450
+ response += `4. If the user cancels, stop without creating anything.\n\n`;
430
451
  response += `For each skill, call \`skill_create\` with:\n`;
431
452
  response += `- name: kebab-case name (e.g. "slack-message-check")\n`;
432
453
  response += `- description: one-line description\n`;
433
454
  response += `- instructions: detailed step-by-step markdown instructions the agent can follow\n`;
434
455
  response += `- emoji: a single emoji representing the skill\n\n`;
435
- response += `After creating each skill, call \`skill_add\` with the returned skill ID to add it to the user's collection.\n\n`;
436
- response += `After ALL skills are created and added, call \`skill_link_job\` with job_name="${args.job_name}" and the list of created skill names to link them and mark the job as analyzed.\n\n`;
456
+ response += `skill_create automatically adds the skill to the user's collection no need to call skill_add.\n\n`;
457
+ response += `After ALL skills are created, call \`skill_link_job\` with job_name="${args.job_name}" and the list of created skill names to link them and mark the job as analyzed.\n\n`;
437
458
  response += `**Guidelines for skill instructions:**\n`;
438
459
  response += `- Write clear, actionable markdown steps\n`;
439
460
  response += `- Reference browser tools (browser_navigate, browser_click, browser_read_page, etc.) for web tasks\n`;
@@ -584,6 +605,86 @@ export function createAgentToolsServer(deps: AgentToolsDeps): McpSdkServerConfig
584
605
  }
585
606
  ),
586
607
 
608
+ // ── User Confirmation Tool ─────────────────────────────────
609
+
610
+ tool(
611
+ "request_user_confirmation",
612
+ "Pause and ask the user for approval or input via the web UI. " +
613
+ "Returns the user's response. Use this before creating skills, making irreversible changes, etc.",
614
+ {
615
+ message: z.string().describe("What to show the user (supports markdown)"),
616
+ options: z.array(z.object({
617
+ label: z.string(),
618
+ value: z.string(),
619
+ description: z.string().optional(),
620
+ })).describe("Buttons/options to show the user"),
621
+ timeout_seconds: z.number().optional().describe("How long to wait for response (default: 300)"),
622
+ },
623
+ async (args) => {
624
+ const actionId = crypto.randomUUID();
625
+ const timeout = (args.timeout_seconds || 300) * 1000;
626
+
627
+ const actionData = {
628
+ id: actionId,
629
+ type: "confirmation",
630
+ message: args.message,
631
+ options: args.options,
632
+ created_at: new Date().toISOString(),
633
+ };
634
+
635
+ try {
636
+ // 1. Store action request in message metadata via RPC
637
+ await setActionRequest(taskId, actionData);
638
+
639
+ // 2. Emit user_action_request event so web UI renders buttons
640
+ await emitEvent(taskId, "user_action_request", actionData);
641
+
642
+ // 3. Poll for response
643
+ const startTime = Date.now();
644
+ const pollInterval = 2000;
645
+
646
+ while (Date.now() - startTime < timeout) {
647
+ const response = await pollActionResponse(taskId);
648
+ if (response) {
649
+ log.info(`User responded to action request ${actionId}: ${JSON.stringify(response)}`);
650
+ return {
651
+ content: [{
652
+ type: "text",
653
+ text: JSON.stringify({
654
+ status: "responded",
655
+ action: response.action || response.value,
656
+ response,
657
+ }),
658
+ }],
659
+ };
660
+ }
661
+
662
+ // Wait before next poll
663
+ await new Promise((resolve) => setTimeout(resolve, pollInterval));
664
+ }
665
+
666
+ // Timeout
667
+ log.warn(`Action request ${actionId} timed out after ${args.timeout_seconds || 300}s`);
668
+ return {
669
+ content: [{
670
+ type: "text",
671
+ text: JSON.stringify({
672
+ status: "timeout",
673
+ message: "User did not respond within the timeout period.",
674
+ }),
675
+ }],
676
+ };
677
+ } catch (err) {
678
+ return {
679
+ content: [{
680
+ type: "text",
681
+ text: `Failed to request user confirmation: ${err instanceof Error ? err.message : err}`,
682
+ }],
683
+ };
684
+ }
685
+ }
686
+ ),
687
+
587
688
  // ── Job Automation Tools ──────────────────────────────────────
588
689
 
589
690
  tool(
@@ -824,54 +925,19 @@ async function saveJobToDb(
824
925
  try {
825
926
  const sb = getSupabase();
826
927
 
827
- // Upsert the job
828
- const { data: job, error: jobError } = await sb
829
- .from("agent_jobs")
830
- .upsert(
831
- { user_id: userId, name: jobName, description: jobDescription },
832
- { onConflict: "user_id,name" }
833
- )
834
- .select("id")
835
- .single();
836
-
837
- if (jobError || !job) {
838
- log.debug(`Failed to save job "${jobName}": ${jobError?.message}`);
839
- return;
840
- }
841
-
842
- // Query DB for skill IDs by name (can't rely on in-memory dbId due to async sync)
843
- if (createdSkillNames.length > 0) {
844
- const { data: skillRows } = await sb
845
- .from("agent_skills")
846
- .select("id")
847
- .eq("user_id", userId)
848
- .in("name", createdSkillNames);
849
-
850
- const skillIds = (skillRows || []).map((r: Record<string, unknown>) => r.id as string);
928
+ const { data, error } = await sb.rpc("save_job_with_skills", {
929
+ p_user_id: userId,
930
+ p_job_name: jobName,
931
+ p_job_description: jobDescription,
932
+ p_skill_names: createdSkillNames,
933
+ });
851
934
 
852
- if (skillIds.length > 0) {
853
- const links = skillIds.map((skillId) => ({
854
- job_id: job.id,
855
- skill_id: skillId,
856
- }));
857
-
858
- const { error: linkError } = await sb
859
- .from("agent_job_skills")
860
- .upsert(links, { onConflict: "job_id,skill_id" });
861
-
862
- if (linkError) {
863
- log.debug(`Failed to link skills to job: ${linkError.message}`);
864
- }
865
- }
935
+ if (error) {
936
+ log.debug(`Failed to save job "${jobName}" via RPC: ${error.message}`);
937
+ return;
866
938
  }
867
939
 
868
- // Mark job as analyzed
869
- await sb
870
- .from("agent_jobs")
871
- .update({ skills_analyzed: true })
872
- .eq("id", job.id);
873
-
874
- log.debug(`Job "${jobName}" saved to DB with ${createdSkillNames.length} skill link(s), marked as analyzed`);
940
+ log.debug(`Job "${jobName}" saved via RPC (id: ${data}), ${createdSkillNames.length} skill(s) linked`);
875
941
  } catch (err) {
876
942
  log.debug(`saveJobToDb error: ${err}`);
877
943
  }
@@ -237,6 +237,8 @@ export class TaskProcessor {
237
237
  "mcp__assistme-agent__skill_browse",
238
238
  "mcp__assistme-agent__skill_add",
239
239
  "mcp__assistme-agent__skill_publish",
240
+ // User confirmation
241
+ "mcp__assistme-agent__request_user_confirmation",
240
242
  // Job automation tools
241
243
  "mcp__assistme-agent__job_run",
242
244
  "mcp__assistme-agent__job_schedule",
@@ -418,7 +418,8 @@ export type EventType =
418
418
  | "tool_result"
419
419
  | "thinking"
420
420
  | "error"
421
- | "status_change";
421
+ | "status_change"
422
+ | "user_action_request";
422
423
 
423
424
  let eventSequence = 0;
424
425
 
@@ -459,3 +460,30 @@ export async function emitEvents(
459
460
  });
460
461
  if (error) log.warn(`Failed to emit events batch: ${error.message}`);
461
462
  }
463
+
464
+ // ── Action Request Helpers ──────────────────────────────────────────
465
+
466
+ export async function setActionRequest(
467
+ messageId: string,
468
+ actionData: Record<string, unknown>
469
+ ): Promise<void> {
470
+ const sb = getSupabase();
471
+ const { error } = await sb.rpc("mcp_set_action_request", {
472
+ p_token_hash: getTokenHash(),
473
+ p_message_id: messageId,
474
+ p_action_data: actionData,
475
+ });
476
+ if (error) throw new Error(`Failed to set action request: ${error.message}`);
477
+ }
478
+
479
+ export async function pollActionResponse(
480
+ messageId: string
481
+ ): Promise<Record<string, unknown> | null> {
482
+ const sb = getSupabase();
483
+ const { data, error } = await sb.rpc("mcp_poll_action_response", {
484
+ p_token_hash: getTokenHash(),
485
+ p_message_id: messageId,
486
+ });
487
+ if (error) throw new Error(`Failed to poll action response: ${error.message}`);
488
+ return data as Record<string, unknown> | null;
489
+ }