assistme 0.8.8 → 0.8.10

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
@@ -1290,6 +1290,64 @@ var SessionHeartbeat = class {
1290
1290
  }
1291
1291
  };
1292
1292
 
1293
+ // src/agent/title-generator.ts
1294
+ import {
1295
+ query as query3
1296
+ } from "@anthropic-ai/claude-agent-sdk";
1297
+ var TITLE_OUTPUT_FORMAT = {
1298
+ type: "json_schema",
1299
+ schema: {
1300
+ type: "object",
1301
+ properties: {
1302
+ title: {
1303
+ type: "string",
1304
+ description: "A very short title (max 6 words) for the task conversation"
1305
+ }
1306
+ },
1307
+ required: ["title"],
1308
+ additionalProperties: false
1309
+ }
1310
+ };
1311
+ async function generateTitle(prompt) {
1312
+ if (!prompt.trim()) return "Untitled";
1313
+ const systemPrompt = "Generate a very short title (max 6 words) for a task conversation based on the user's prompt. Return ONLY the title, no quotes, no punctuation at the end. If the prompt is in Chinese, generate a Chinese title.";
1314
+ let title = "Untitled";
1315
+ for await (const message of query3({
1316
+ prompt,
1317
+ options: {
1318
+ model: "claude-haiku-4-5-20251001",
1319
+ systemPrompt,
1320
+ allowedTools: [],
1321
+ effort: "low",
1322
+ outputFormat: TITLE_OUTPUT_FORMAT
1323
+ }
1324
+ })) {
1325
+ if (message.type === "result") {
1326
+ const resultMsg = message;
1327
+ if (resultMsg.subtype === "success") {
1328
+ const successMsg = resultMsg;
1329
+ const output = successMsg.structured_output;
1330
+ if (output?.title) {
1331
+ title = String(output.title).trim().slice(0, 100);
1332
+ }
1333
+ }
1334
+ }
1335
+ }
1336
+ return title;
1337
+ }
1338
+ async function generateAndSaveTitle(conversationId, prompt) {
1339
+ try {
1340
+ const title = await generateTitle(prompt);
1341
+ await callMcpHandler("conversation.update_title", {
1342
+ conversation_id: conversationId,
1343
+ title
1344
+ });
1345
+ log.info(`Generated title: "${title}"`);
1346
+ } catch (err) {
1347
+ log.debug(`Title generation failed (non-critical): ${err}`);
1348
+ }
1349
+ }
1350
+
1293
1351
  // src/orchestrator.ts
1294
1352
  var Orchestrator = class {
1295
1353
  session = null;
@@ -1303,10 +1361,12 @@ var Orchestrator = class {
1303
1361
  workerManager = null;
1304
1362
  userId = null;
1305
1363
  conversationId = null;
1364
+ titledConversations = /* @__PURE__ */ new Set();
1306
1365
  // ── Lifecycle ───────────────────────────────────────────────────
1307
1366
  async start(userId) {
1308
1367
  const config = getConfig();
1309
1368
  this.userId = userId;
1369
+ this.titledConversations.clear();
1310
1370
  this.session = await createSession(config.sessionName, config.workspacePath, "0.1.0");
1311
1371
  this.conversationId = await getOrCreateCliConversation();
1312
1372
  this.workerManager = new WorkerManager(userId, this.session.id);
@@ -1319,6 +1379,12 @@ var Orchestrator = class {
1319
1379
  this.heartbeat.start(this.session.id);
1320
1380
  this.taskPoller = new TaskPoller(this.session.id, {
1321
1381
  onTask: (task) => {
1382
+ if (task.conversation_id && !this.titledConversations.has(task.conversation_id)) {
1383
+ this.titledConversations.add(task.conversation_id);
1384
+ generateAndSaveTitle(task.conversation_id, task.prompt).catch(
1385
+ (err) => log.debug(`Title generation failed: ${err}`)
1386
+ );
1387
+ }
1322
1388
  this.workerManager.dispatchTask(task).catch((err) => {
1323
1389
  log.error(`Task dispatch error: ${err}`);
1324
1390
  });
@@ -1377,6 +1443,12 @@ var Orchestrator = class {
1377
1443
  }
1378
1444
  const task = await createTask(this.conversationId, this.session.id, prompt);
1379
1445
  await claimTask(task.id);
1446
+ if (!this.titledConversations.has(this.conversationId)) {
1447
+ this.titledConversations.add(this.conversationId);
1448
+ generateAndSaveTitle(this.conversationId, prompt).catch(
1449
+ (err) => log.debug(`Title generation failed: ${err}`)
1450
+ );
1451
+ }
1380
1452
  await this.workerManager.dispatchTask(task);
1381
1453
  }
1382
1454
  async dispatchAndWait(prompt) {
@@ -1385,6 +1457,12 @@ var Orchestrator = class {
1385
1457
  }
1386
1458
  const task = await createTask(this.conversationId, this.session.id, prompt);
1387
1459
  await claimTask(task.id);
1460
+ if (!this.titledConversations.has(this.conversationId)) {
1461
+ this.titledConversations.add(this.conversationId);
1462
+ generateAndSaveTitle(this.conversationId, prompt).catch(
1463
+ (err) => log.debug(`Title generation failed: ${err}`)
1464
+ );
1465
+ }
1388
1466
  await this.workerManager.dispatchAndWait(task);
1389
1467
  }
1390
1468
  /** Alias for interactive CLI. */
@@ -1979,17 +2057,17 @@ Memories (${memories.length}):`));
1979
2057
  process.exit(1);
1980
2058
  }
1981
2059
  });
1982
- memoryCmd.command("search <query>").description("Search memories").action(async (query3) => {
2060
+ memoryCmd.command("search <query>").description("Search memories").action(async (query4) => {
1983
2061
  try {
1984
2062
  await getCurrentUserId();
1985
2063
  const mm = new MemoryManager();
1986
- const results = await mm.search(query3);
2064
+ const results = await mm.search(query4);
1987
2065
  if (results.length === 0) {
1988
- console.log(chalk8.yellow(`No memories matching "${query3}"`));
2066
+ console.log(chalk8.yellow(`No memories matching "${query4}"`));
1989
2067
  return;
1990
2068
  }
1991
2069
  console.log(chalk8.bold(`
1992
- Search results for "${query3}":`));
2070
+ Search results for "${query4}":`));
1993
2071
  for (const m of results) {
1994
2072
  console.log(` [${m.category}] ${m.content}`);
1995
2073
  }
@@ -2055,18 +2133,18 @@ function registerSkillCommands(program2) {
2055
2133
  );
2056
2134
  console.log();
2057
2135
  });
2058
- skillCmd.command("search <query>").description("Search skills in your collection and marketplace").action(async (query3) => {
2136
+ skillCmd.command("search <query>").description("Search skills in your collection and marketplace").action(async (query4) => {
2059
2137
  const spinner = ora4("Searching skills...").start();
2060
2138
  const sm = await getAuthenticatedSkillManager();
2061
2139
  try {
2062
- const results = await sm.searchDb(query3);
2140
+ const results = await sm.searchDb(query4);
2063
2141
  spinner.stop();
2064
2142
  if (results.length === 0) {
2065
- console.log(chalk9.yellow(`No skills found for "${query3}"`));
2143
+ console.log(chalk9.yellow(`No skills found for "${query4}"`));
2066
2144
  return;
2067
2145
  }
2068
2146
  console.log(chalk9.bold(`
2069
- Skills matching "${query3}":`));
2147
+ Skills matching "${query4}":`));
2070
2148
  for (const r of results) {
2071
2149
  const emoji = r.emoji ? `${r.emoji} ` : "";
2072
2150
  console.log(` ${emoji}${chalk9.cyan(r.name)} [${r.source}]`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "assistme",
3
- "version": "0.8.8",
3
+ "version": "0.8.10",
4
4
  "description": "AssistMe CLI Agent - AI-powered agentic assistant for code, browser, and automation",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -0,0 +1,82 @@
1
+ import {
2
+ query,
3
+ type SDKResultMessage,
4
+ type SDKResultSuccess,
5
+ type OutputFormat,
6
+ } from "@anthropic-ai/claude-agent-sdk";
7
+ import { callMcpHandler } from "../db/api-client.js";
8
+ import { log } from "../utils/logger.js";
9
+
10
+ /**
11
+ * Structured output schema for title generation.
12
+ */
13
+ const TITLE_OUTPUT_FORMAT: OutputFormat = {
14
+ type: "json_schema",
15
+ schema: {
16
+ type: "object",
17
+ properties: {
18
+ title: {
19
+ type: "string",
20
+ description: "A very short title (max 6 words) for the task conversation",
21
+ },
22
+ },
23
+ required: ["title"],
24
+ additionalProperties: false,
25
+ },
26
+ };
27
+
28
+ /**
29
+ * Generate a short conversation title from the user's prompt.
30
+ * Uses the Claude Agent SDK query() with structured output.
31
+ */
32
+ export async function generateTitle(prompt: string): Promise<string> {
33
+ if (!prompt.trim()) return "Untitled";
34
+
35
+ const systemPrompt =
36
+ "Generate a very short title (max 6 words) for a task conversation based on the user's prompt. " +
37
+ "Return ONLY the title, no quotes, no punctuation at the end. " +
38
+ "If the prompt is in Chinese, generate a Chinese title.";
39
+
40
+ let title = "Untitled";
41
+
42
+ for await (const message of query({
43
+ prompt,
44
+ options: {
45
+ model: "claude-haiku-4-5-20251001",
46
+ systemPrompt,
47
+ allowedTools: [],
48
+ effort: "low",
49
+ outputFormat: TITLE_OUTPUT_FORMAT,
50
+ },
51
+ })) {
52
+ if (message.type === "result") {
53
+ const resultMsg = message as SDKResultMessage;
54
+ if (resultMsg.subtype === "success") {
55
+ const successMsg = resultMsg as SDKResultSuccess;
56
+ const output = successMsg.structured_output as { title?: string } | undefined;
57
+ if (output?.title) {
58
+ title = String(output.title).trim().slice(0, 100);
59
+ }
60
+ }
61
+ }
62
+ }
63
+
64
+ return title;
65
+ }
66
+
67
+ /**
68
+ * Generate and save a title for a conversation.
69
+ * Fails silently — title generation is best-effort.
70
+ */
71
+ export async function generateAndSaveTitle(conversationId: string, prompt: string): Promise<void> {
72
+ try {
73
+ const title = await generateTitle(prompt);
74
+ await callMcpHandler("conversation.update_title", {
75
+ conversation_id: conversationId,
76
+ title,
77
+ });
78
+ log.info(`Generated title: "${title}"`);
79
+ } catch (err) {
80
+ log.debug(`Title generation failed (non-critical): ${err}`);
81
+ }
82
+ }
@@ -32,6 +32,7 @@ import { WorkerManager } from "./workers/manager.js";
32
32
  import { JobAnalysisPoller } from "./agent/job-analysis-poller.js";
33
33
  import { TaskPoller } from "./agent/task-poller.js";
34
34
  import { SessionHeartbeat } from "./agent/session-heartbeat.js";
35
+ import { generateAndSaveTitle } from "./agent/title-generator.js";
35
36
 
36
37
  export class Orchestrator {
37
38
  private session: AgentSession | null = null;
@@ -47,12 +48,14 @@ export class Orchestrator {
47
48
 
48
49
  private userId: string | null = null;
49
50
  private conversationId: string | null = null;
51
+ private titledConversations = new Set<string>();
50
52
 
51
53
  // ── Lifecycle ───────────────────────────────────────────────────
52
54
 
53
55
  async start(userId: string): Promise<AgentSession> {
54
56
  const config = getConfig();
55
57
  this.userId = userId;
58
+ this.titledConversations.clear();
56
59
 
57
60
  this.session = await createSession(config.sessionName, config.workspacePath, "0.1.0");
58
61
  this.conversationId = await getOrCreateCliConversation();
@@ -71,6 +74,13 @@ export class Orchestrator {
71
74
 
72
75
  this.taskPoller = new TaskPoller(this.session.id, {
73
76
  onTask: (task) => {
77
+ // Generate title on first task per conversation (fire-and-forget)
78
+ if (task.conversation_id && !this.titledConversations.has(task.conversation_id)) {
79
+ this.titledConversations.add(task.conversation_id);
80
+ generateAndSaveTitle(task.conversation_id, task.prompt).catch((err) =>
81
+ log.debug(`Title generation failed: ${err}`)
82
+ );
83
+ }
74
84
  this.workerManager!.dispatchTask(task).catch((err) => {
75
85
  log.error(`Task dispatch error: ${err}`);
76
86
  });
@@ -141,6 +151,15 @@ export class Orchestrator {
141
151
 
142
152
  const task = await createTask(this.conversationId, this.session.id, prompt);
143
153
  await claimTask(task.id);
154
+
155
+ // Generate title on first task per conversation (fire-and-forget)
156
+ if (!this.titledConversations.has(this.conversationId)) {
157
+ this.titledConversations.add(this.conversationId);
158
+ generateAndSaveTitle(this.conversationId, prompt).catch((err) =>
159
+ log.debug(`Title generation failed: ${err}`)
160
+ );
161
+ }
162
+
144
163
  await this.workerManager!.dispatchTask(task);
145
164
  }
146
165
 
@@ -151,6 +170,15 @@ export class Orchestrator {
151
170
 
152
171
  const task = await createTask(this.conversationId, this.session.id, prompt);
153
172
  await claimTask(task.id);
173
+
174
+ // Generate title on first task per conversation (fire-and-forget)
175
+ if (!this.titledConversations.has(this.conversationId)) {
176
+ this.titledConversations.add(this.conversationId);
177
+ generateAndSaveTitle(this.conversationId, prompt).catch((err) =>
178
+ log.debug(`Title generation failed: ${err}`)
179
+ );
180
+ }
181
+
154
182
  await this.workerManager!.dispatchAndWait(task);
155
183
  }
156
184
 
@@ -0,0 +1,196 @@
1
+ import { describe, it, expect, vi, beforeEach } from "vitest";
2
+
3
+ // ── Mocks ────────────────────────────────────────────────────────
4
+
5
+ let mockQueryCalls: unknown[] = [];
6
+ const mockQueryMessages: unknown[] = [];
7
+ vi.mock("@anthropic-ai/claude-agent-sdk", () => ({
8
+ query: vi.fn((params: unknown) => {
9
+ mockQueryCalls.push(params);
10
+ return {
11
+ [Symbol.asyncIterator]: async function* () {
12
+ for (const msg of mockQueryMessages) {
13
+ yield msg;
14
+ }
15
+ },
16
+ };
17
+ }),
18
+ }));
19
+
20
+ const mockCallMcpHandler = vi.fn().mockResolvedValue(null);
21
+ vi.mock("../../src/db/api-client.js", () => ({
22
+ callMcpHandler: (...args: unknown[]) => mockCallMcpHandler(...args),
23
+ }));
24
+
25
+ vi.mock("../../src/utils/logger.js", () => ({
26
+ log: {
27
+ debug: vi.fn(),
28
+ info: vi.fn(),
29
+ warn: vi.fn(),
30
+ error: vi.fn(),
31
+ },
32
+ }));
33
+
34
+ // ── Import after mocks ──────────────────────────────────────────
35
+
36
+ import { generateTitle, generateAndSaveTitle } from "../../src/agent/title-generator.js";
37
+ import { query } from "@anthropic-ai/claude-agent-sdk";
38
+
39
+ // ── Tests ────────────────────────────────────────────────────────
40
+
41
+ describe("title-generator", () => {
42
+ beforeEach(() => {
43
+ vi.clearAllMocks();
44
+ mockQueryCalls = [];
45
+ mockQueryMessages.length = 0;
46
+ });
47
+
48
+ describe("generateTitle", () => {
49
+ it("returns Untitled for empty prompt", async () => {
50
+ const title = await generateTitle("");
51
+ expect(title).toBe("Untitled");
52
+ expect(query).not.toHaveBeenCalled();
53
+ });
54
+
55
+ it("returns Untitled for whitespace-only prompt", async () => {
56
+ const title = await generateTitle(" ");
57
+ expect(title).toBe("Untitled");
58
+ expect(query).not.toHaveBeenCalled();
59
+ });
60
+
61
+ it("passes correct SDK query options", async () => {
62
+ mockQueryMessages.push({
63
+ type: "result",
64
+ subtype: "success",
65
+ structured_output: { title: "Fix Login Bug" },
66
+ });
67
+
68
+ await generateTitle("The login button is broken");
69
+
70
+ expect(mockQueryCalls).toHaveLength(1);
71
+ const call = mockQueryCalls[0] as { prompt: string; options: Record<string, unknown> };
72
+ expect(call.prompt).toBe("The login button is broken");
73
+ expect(call.options.model).toBe("claude-haiku-4-5-20251001");
74
+ expect(call.options.allowedTools).toEqual([]);
75
+ expect(call.options.effort).toBe("low");
76
+ expect(call.options.outputFormat).toBeDefined();
77
+ });
78
+
79
+ it("parses valid structured output title", async () => {
80
+ mockQueryMessages.push({
81
+ type: "result",
82
+ subtype: "success",
83
+ structured_output: { title: "Fix Login Bug" },
84
+ });
85
+
86
+ const title = await generateTitle("The login button is broken");
87
+ expect(title).toBe("Fix Login Bug");
88
+ });
89
+
90
+ it("returns Untitled when structured_output is missing title field", async () => {
91
+ mockQueryMessages.push({
92
+ type: "result",
93
+ subtype: "success",
94
+ structured_output: {},
95
+ });
96
+
97
+ const title = await generateTitle("Some prompt");
98
+ expect(title).toBe("Untitled");
99
+ });
100
+
101
+ it("returns Untitled when structured_output is undefined", async () => {
102
+ mockQueryMessages.push({
103
+ type: "result",
104
+ subtype: "success",
105
+ structured_output: undefined,
106
+ });
107
+
108
+ const title = await generateTitle("Some prompt");
109
+ expect(title).toBe("Untitled");
110
+ });
111
+
112
+ it("truncates title longer than 100 characters", async () => {
113
+ const longTitle = "A".repeat(150);
114
+ mockQueryMessages.push({
115
+ type: "result",
116
+ subtype: "success",
117
+ structured_output: { title: longTitle },
118
+ });
119
+
120
+ const title = await generateTitle("Some prompt");
121
+ expect(title).toHaveLength(100);
122
+ expect(title).toBe("A".repeat(100));
123
+ });
124
+
125
+ it("trims whitespace from title", async () => {
126
+ mockQueryMessages.push({
127
+ type: "result",
128
+ subtype: "success",
129
+ structured_output: { title: " Fix Bug " },
130
+ });
131
+
132
+ const title = await generateTitle("Some prompt");
133
+ expect(title).toBe("Fix Bug");
134
+ });
135
+
136
+ it("returns Untitled on SDK error (non-success result)", async () => {
137
+ mockQueryMessages.push({
138
+ type: "result",
139
+ subtype: "error",
140
+ });
141
+
142
+ const title = await generateTitle("Some prompt");
143
+ expect(title).toBe("Untitled");
144
+ });
145
+
146
+ it("ignores non-result messages", async () => {
147
+ mockQueryMessages.push(
148
+ { type: "assistant", content: [{ type: "text", text: "hello" }] },
149
+ {
150
+ type: "result",
151
+ subtype: "success",
152
+ structured_output: { title: "Real Title" },
153
+ }
154
+ );
155
+
156
+ const title = await generateTitle("Some prompt");
157
+ expect(title).toBe("Real Title");
158
+ });
159
+ });
160
+
161
+ describe("generateAndSaveTitle", () => {
162
+ it("generates title and saves via MCP handler", async () => {
163
+ mockQueryMessages.push({
164
+ type: "result",
165
+ subtype: "success",
166
+ structured_output: { title: "Deploy Dashboard" },
167
+ });
168
+
169
+ await generateAndSaveTitle("conv-123", "Deploy the dashboard");
170
+
171
+ expect(mockCallMcpHandler).toHaveBeenCalledWith("conversation.update_title", {
172
+ conversation_id: "conv-123",
173
+ title: "Deploy Dashboard",
174
+ });
175
+ });
176
+
177
+ it("does not throw on SDK failure (graceful degradation)", async () => {
178
+ (query as ReturnType<typeof vi.fn>).mockImplementationOnce(() => {
179
+ throw new Error("SDK unavailable");
180
+ });
181
+
182
+ await expect(generateAndSaveTitle("conv-123", "test")).resolves.toBeUndefined();
183
+ });
184
+
185
+ it("does not throw on MCP handler failure", async () => {
186
+ mockQueryMessages.push({
187
+ type: "result",
188
+ subtype: "success",
189
+ structured_output: { title: "Some Title" },
190
+ });
191
+ mockCallMcpHandler.mockRejectedValueOnce(new Error("MCP failure"));
192
+
193
+ await expect(generateAndSaveTitle("conv-123", "test")).resolves.toBeUndefined();
194
+ });
195
+ });
196
+ });