assistme 0.8.8 → 0.8.9

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
+ titleGenerated = false;
1306
1365
  // ── Lifecycle ───────────────────────────────────────────────────
1307
1366
  async start(userId) {
1308
1367
  const config = getConfig();
1309
1368
  this.userId = userId;
1369
+ this.titleGenerated = false;
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);
@@ -1377,6 +1437,12 @@ var Orchestrator = class {
1377
1437
  }
1378
1438
  const task = await createTask(this.conversationId, this.session.id, prompt);
1379
1439
  await claimTask(task.id);
1440
+ if (!this.titleGenerated) {
1441
+ this.titleGenerated = true;
1442
+ generateAndSaveTitle(this.conversationId, prompt).catch(
1443
+ (err) => log.debug(`Title generation failed: ${err}`)
1444
+ );
1445
+ }
1380
1446
  await this.workerManager.dispatchTask(task);
1381
1447
  }
1382
1448
  async dispatchAndWait(prompt) {
@@ -1979,17 +2045,17 @@ Memories (${memories.length}):`));
1979
2045
  process.exit(1);
1980
2046
  }
1981
2047
  });
1982
- memoryCmd.command("search <query>").description("Search memories").action(async (query3) => {
2048
+ memoryCmd.command("search <query>").description("Search memories").action(async (query4) => {
1983
2049
  try {
1984
2050
  await getCurrentUserId();
1985
2051
  const mm = new MemoryManager();
1986
- const results = await mm.search(query3);
2052
+ const results = await mm.search(query4);
1987
2053
  if (results.length === 0) {
1988
- console.log(chalk8.yellow(`No memories matching "${query3}"`));
2054
+ console.log(chalk8.yellow(`No memories matching "${query4}"`));
1989
2055
  return;
1990
2056
  }
1991
2057
  console.log(chalk8.bold(`
1992
- Search results for "${query3}":`));
2058
+ Search results for "${query4}":`));
1993
2059
  for (const m of results) {
1994
2060
  console.log(` [${m.category}] ${m.content}`);
1995
2061
  }
@@ -2055,18 +2121,18 @@ function registerSkillCommands(program2) {
2055
2121
  );
2056
2122
  console.log();
2057
2123
  });
2058
- skillCmd.command("search <query>").description("Search skills in your collection and marketplace").action(async (query3) => {
2124
+ skillCmd.command("search <query>").description("Search skills in your collection and marketplace").action(async (query4) => {
2059
2125
  const spinner = ora4("Searching skills...").start();
2060
2126
  const sm = await getAuthenticatedSkillManager();
2061
2127
  try {
2062
- const results = await sm.searchDb(query3);
2128
+ const results = await sm.searchDb(query4);
2063
2129
  spinner.stop();
2064
2130
  if (results.length === 0) {
2065
- console.log(chalk9.yellow(`No skills found for "${query3}"`));
2131
+ console.log(chalk9.yellow(`No skills found for "${query4}"`));
2066
2132
  return;
2067
2133
  }
2068
2134
  console.log(chalk9.bold(`
2069
- Skills matching "${query3}":`));
2135
+ Skills matching "${query4}":`));
2070
2136
  for (const r of results) {
2071
2137
  const emoji = r.emoji ? `${r.emoji} ` : "";
2072
2138
  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.9",
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 titleGenerated = false;
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.titleGenerated = false;
56
59
 
57
60
  this.session = await createSession(config.sessionName, config.workspacePath, "0.1.0");
58
61
  this.conversationId = await getOrCreateCliConversation();
@@ -141,6 +144,15 @@ export class Orchestrator {
141
144
 
142
145
  const task = await createTask(this.conversationId, this.session.id, prompt);
143
146
  await claimTask(task.id);
147
+
148
+ // Generate title on first task (fire-and-forget, non-blocking)
149
+ if (!this.titleGenerated) {
150
+ this.titleGenerated = true;
151
+ generateAndSaveTitle(this.conversationId, prompt).catch((err) =>
152
+ log.debug(`Title generation failed: ${err}`)
153
+ );
154
+ }
155
+
144
156
  await this.workerManager!.dispatchTask(task);
145
157
  }
146
158
 
@@ -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
+ });