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 +74 -8
- package/package.json +1 -1
- package/src/agent/title-generator.ts +82 -0
- package/src/orchestrator.ts +12 -0
- package/tests/agent/title-generator.test.ts +196 -0
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 (
|
|
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(
|
|
2052
|
+
const results = await mm.search(query4);
|
|
1987
2053
|
if (results.length === 0) {
|
|
1988
|
-
console.log(chalk8.yellow(`No memories matching "${
|
|
2054
|
+
console.log(chalk8.yellow(`No memories matching "${query4}"`));
|
|
1989
2055
|
return;
|
|
1990
2056
|
}
|
|
1991
2057
|
console.log(chalk8.bold(`
|
|
1992
|
-
Search results for "${
|
|
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 (
|
|
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(
|
|
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 "${
|
|
2131
|
+
console.log(chalk9.yellow(`No skills found for "${query4}"`));
|
|
2066
2132
|
return;
|
|
2067
2133
|
}
|
|
2068
2134
|
console.log(chalk9.bold(`
|
|
2069
|
-
Skills matching "${
|
|
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
|
@@ -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
|
+
}
|
package/src/orchestrator.ts
CHANGED
|
@@ -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
|
+
});
|