@love-moon/ai-sdk 0.14.0 → 0.15.1
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/CHANGELOG.md +29 -0
- package/dist/providers/codex-app-server-session.js +1 -0
- package/dist/resume/claude.d.ts +8 -0
- package/dist/resume/claude.js +28 -1
- package/dist/resume/codex.d.ts +8 -0
- package/dist/resume/codex.js +37 -1
- package/dist/resume/index.d.ts +8 -0
- package/dist/resume/shared.d.ts +18 -0
- package/dist/resume/shared.js +65 -1
- package/dist/transports/codex-app-server-transport.js +22 -1
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,34 @@
|
|
|
1
1
|
# @love-moon/ai-sdk
|
|
2
2
|
|
|
3
|
+
## 0.15.1
|
|
4
|
+
|
|
5
|
+
## 0.15.0
|
|
6
|
+
|
|
7
|
+
### Minor Changes
|
|
8
|
+
|
|
9
|
+
- cc1e290: New Terminal and Resume Session previews in the create-task dialog.
|
|
10
|
+
|
|
11
|
+
- `@love-moon/ai-sdk`: `listSessions` (claude, codex) returns `preview` with
|
|
12
|
+
the first user message, the assistant reply to it, and the session's last
|
|
13
|
+
message.
|
|
14
|
+
- The daemon passes the preview to `list_backend_sessions` as
|
|
15
|
+
`first_user_message`, `first_reply`, `last_message` and `last_message_role`.
|
|
16
|
+
- A PTY task with no project path now starts its shell in `$HOME`. The
|
|
17
|
+
terminal log stays in the dated workspace directory.
|
|
18
|
+
|
|
19
|
+
- 55b65db: `conductor remote mcp`: structured tools for a task whose worktree lives on
|
|
20
|
+
another daemon (RFC 0040).
|
|
21
|
+
|
|
22
|
+
- New `conductor remote mcp --host <daemon> --root <dir> [--cwd <dir>]`, a
|
|
23
|
+
stdio MCP server with `remote_read`, `remote_edit`, `remote_write`,
|
|
24
|
+
`remote_grep`, `remote_glob` and `remote_bash`, bound to that directory.
|
|
25
|
+
- For a `launch_config.remoteWorktree` task the daemon passes the binding to
|
|
26
|
+
fire (`CONDUCTOR_REMOTE_WORKTREE`), and fire attaches the server to Claude
|
|
27
|
+
(`mcpServers`) and Codex (`-c mcp_servers.*`). The agent token is forwarded
|
|
28
|
+
by env only, never on argv.
|
|
29
|
+
- `@love-moon/ai-sdk`: the Codex app-server session accepts `configOverrides`,
|
|
30
|
+
passed to the app-server as `-c key=value`.
|
|
31
|
+
|
|
3
32
|
## 0.14.0
|
|
4
33
|
|
|
5
34
|
### Minor Changes
|
|
@@ -307,6 +307,7 @@ export class CodexAppServerSession extends EventEmitter {
|
|
|
307
307
|
},
|
|
308
308
|
commandLine: options.commandLine,
|
|
309
309
|
enableGoals: this.goalMode || options.enableGoals === true,
|
|
310
|
+
configOverrides: options.configOverrides,
|
|
310
311
|
});
|
|
311
312
|
this.transport.on("notification", ({ method, params }) => {
|
|
312
313
|
void this.handleNotification(method, params);
|
package/dist/resume/claude.d.ts
CHANGED
|
@@ -24,5 +24,13 @@ export function listSessions(options?: {}): Promise<{
|
|
|
24
24
|
cwd: any;
|
|
25
25
|
title: string | null;
|
|
26
26
|
updatedAt: any;
|
|
27
|
+
preview: {
|
|
28
|
+
firstUserMessage: string | null;
|
|
29
|
+
firstReply: string | null;
|
|
30
|
+
lastMessage: {
|
|
31
|
+
role: any;
|
|
32
|
+
text: string | null;
|
|
33
|
+
} | null;
|
|
34
|
+
};
|
|
27
35
|
}[]>;
|
|
28
36
|
export const BACKEND: "claude";
|
package/dist/resume/claude.js
CHANGED
|
@@ -3,7 +3,7 @@ import { promises as fsp } from "node:fs";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import readline from "node:readline";
|
|
5
5
|
import { resolveClaudeConfigDirs } from "../manager/paths.js";
|
|
6
|
-
import { buildResumeContext, isExistingDirectory, normalizeSessionId, normalizeSessionTitle, pathExists, readJsonlHeadEntries, resolveSessionRunDirectory, } from "./shared.js";
|
|
6
|
+
import { buildResumeContext, buildSessionPreview, isExistingDirectory, normalizeSessionId, normalizeSessionTitle, pathExists, readJsonlHeadEntries, readJsonlTailEntries, resolveSessionRunDirectory, } from "./shared.js";
|
|
7
7
|
export const BACKEND = "claude";
|
|
8
8
|
export function buildCliArgs(sessionId) {
|
|
9
9
|
const normalizedSessionId = normalizeSessionId(sessionId);
|
|
@@ -156,12 +156,14 @@ export async function listSessions(options = {}) {
|
|
|
156
156
|
const sessions = [];
|
|
157
157
|
for (const candidate of selected) {
|
|
158
158
|
const entries = await readJsonlHeadEntries(candidate.sessionFilePath);
|
|
159
|
+
const tailEntries = await readJsonlTailEntries(candidate.sessionFilePath);
|
|
159
160
|
sessions.push({
|
|
160
161
|
sessionId: candidate.sessionId,
|
|
161
162
|
sessionFilePath: candidate.sessionFilePath,
|
|
162
163
|
cwd: extractHeadCwd(entries),
|
|
163
164
|
title: extractClaudeTitle(entries),
|
|
164
165
|
updatedAt: candidate.updatedAt,
|
|
166
|
+
preview: buildSessionPreview(extractClaudeMessages(entries), extractClaudeMessages(tailEntries)),
|
|
165
167
|
});
|
|
166
168
|
}
|
|
167
169
|
return sessions;
|
|
@@ -174,6 +176,31 @@ function extractHeadCwd(entries) {
|
|
|
174
176
|
}
|
|
175
177
|
return null;
|
|
176
178
|
}
|
|
179
|
+
/**
|
|
180
|
+
* User/assistant text messages. Tool calls, tool results, meta entries and
|
|
181
|
+
* wrapped slash-command output (<command-name>, <local-command-stdout>) are skipped.
|
|
182
|
+
*/
|
|
183
|
+
function extractClaudeMessages(entries) {
|
|
184
|
+
const messages = [];
|
|
185
|
+
for (const entry of entries) {
|
|
186
|
+
if ((entry?.type !== "user" && entry?.type !== "assistant") || entry?.isMeta === true) {
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
const content = entry?.message?.content;
|
|
190
|
+
const text = typeof content === "string"
|
|
191
|
+
? content
|
|
192
|
+
: Array.isArray(content)
|
|
193
|
+
? content
|
|
194
|
+
.filter((item) => item?.type === "text" && typeof item.text === "string")
|
|
195
|
+
.map((item) => item.text)
|
|
196
|
+
.join("\n")
|
|
197
|
+
: "";
|
|
198
|
+
if (text.trim() && !text.trim().startsWith("<")) {
|
|
199
|
+
messages.push({ role: entry.type, text });
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
return messages;
|
|
203
|
+
}
|
|
177
204
|
function extractClaudeTitle(entries) {
|
|
178
205
|
for (const entry of entries) {
|
|
179
206
|
if (entry?.type !== "user" || entry?.isMeta === true) {
|
package/dist/resume/codex.d.ts
CHANGED
|
@@ -24,5 +24,13 @@ export function listSessions(options?: {}): Promise<{
|
|
|
24
24
|
cwd: string | null;
|
|
25
25
|
title: string | null;
|
|
26
26
|
updatedAt: number;
|
|
27
|
+
preview: {
|
|
28
|
+
firstUserMessage: string | null;
|
|
29
|
+
firstReply: string | null;
|
|
30
|
+
lastMessage: {
|
|
31
|
+
role: any;
|
|
32
|
+
text: string | null;
|
|
33
|
+
} | null;
|
|
34
|
+
};
|
|
27
35
|
}[]>;
|
|
28
36
|
export const BACKEND: "codex";
|
package/dist/resume/codex.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { promises as fsp } from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import { buildResumeContext, isExistingDirectory, iterateJsonlEntries, normalizeSessionId, normalizeSessionTitle, readJsonlHeadEntries, resolveHomeDir, resolveSessionRunDirectory, } from "./shared.js";
|
|
3
|
+
import { buildResumeContext, buildSessionPreview, isExistingDirectory, iterateJsonlEntries, normalizeSessionId, normalizeSessionTitle, readJsonlHeadEntries, readJsonlTailEntries, resolveHomeDir, resolveSessionRunDirectory, } from "./shared.js";
|
|
4
4
|
export const BACKEND = "codex";
|
|
5
5
|
export function buildCliArgs(sessionId) {
|
|
6
6
|
const normalizedSessionId = normalizeSessionId(sessionId);
|
|
@@ -111,12 +111,14 @@ export async function listSessions(options = {}) {
|
|
|
111
111
|
continue;
|
|
112
112
|
}
|
|
113
113
|
const metaCwd = meta?.payload?.cwd;
|
|
114
|
+
const tailEntries = await readJsonlTailEntries(candidate.filePath);
|
|
114
115
|
sessions.push({
|
|
115
116
|
sessionId,
|
|
116
117
|
sessionFilePath: candidate.filePath,
|
|
117
118
|
cwd: typeof metaCwd === "string" && metaCwd.trim() ? metaCwd.trim() : null,
|
|
118
119
|
title: extractCodexTitle(headEntries),
|
|
119
120
|
updatedAt: candidate.updatedAt,
|
|
121
|
+
preview: buildSessionPreview(extractCodexMessages(headEntries), extractCodexMessages(tailEntries)),
|
|
120
122
|
});
|
|
121
123
|
}
|
|
122
124
|
return sessions;
|
|
@@ -126,6 +128,40 @@ function extractSessionIdFromFileName(filePath) {
|
|
|
126
128
|
const match = path.basename(filePath).match(SESSION_ID_IN_FILE_NAME);
|
|
127
129
|
return match ? match[0] : "";
|
|
128
130
|
}
|
|
131
|
+
// The event stream's user/agent messages: what the user typed and the
|
|
132
|
+
// assistant's replies, without injected context or tool output.
|
|
133
|
+
const CODEX_MESSAGE_ROLES = { user_message: "user", agent_message: "assistant" };
|
|
134
|
+
const CODEX_ITEM_TEXT_TYPES = { user: "input_text", assistant: "output_text" };
|
|
135
|
+
function extractCodexMessages(entries) {
|
|
136
|
+
const messages = [];
|
|
137
|
+
for (const entry of entries) {
|
|
138
|
+
const role = entry?.type === "event_msg" ? CODEX_MESSAGE_ROLES[entry?.payload?.type] : null;
|
|
139
|
+
const text = entry?.payload?.message;
|
|
140
|
+
if (role && typeof text === "string" && text.trim()) {
|
|
141
|
+
messages.push({ role, text });
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
if (messages.length > 0) {
|
|
145
|
+
return messages;
|
|
146
|
+
}
|
|
147
|
+
// Sessions without those events only carry response items; skip injected
|
|
148
|
+
// wrappers such as <environment_context>, as the title does.
|
|
149
|
+
for (const entry of entries) {
|
|
150
|
+
const payload = entry?.type === "response_item" ? entry.payload : null;
|
|
151
|
+
const textType = payload?.type === "message" ? CODEX_ITEM_TEXT_TYPES[payload.role] : null;
|
|
152
|
+
if (!textType || !Array.isArray(payload.content)) {
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
const text = payload.content
|
|
156
|
+
.filter((item) => item?.type === textType && typeof item.text === "string")
|
|
157
|
+
.map((item) => item.text)
|
|
158
|
+
.join("\n");
|
|
159
|
+
if (text.trim() && !text.trim().startsWith("<")) {
|
|
160
|
+
messages.push({ role: payload.role, text });
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return messages;
|
|
164
|
+
}
|
|
129
165
|
function extractCodexTitle(entries) {
|
|
130
166
|
for (const entry of entries) {
|
|
131
167
|
if (entry?.type === "event_msg" && entry?.payload?.type === "user_message") {
|
package/dist/resume/index.d.ts
CHANGED
|
@@ -11,6 +11,14 @@ export function listSessionsForBackend(backendType: any, opts?: {}): Promise<{
|
|
|
11
11
|
cwd: string | null;
|
|
12
12
|
title: string | null;
|
|
13
13
|
updatedAt: number;
|
|
14
|
+
preview: {
|
|
15
|
+
firstUserMessage: string | null;
|
|
16
|
+
firstReply: string | null;
|
|
17
|
+
lastMessage: {
|
|
18
|
+
role: any;
|
|
19
|
+
text: string | null;
|
|
20
|
+
} | null;
|
|
21
|
+
};
|
|
14
22
|
}[]>;
|
|
15
23
|
export function resolveResumeContext(backend: any, sessionId: any, options?: {}): Promise<{
|
|
16
24
|
provider: any;
|
package/dist/resume/shared.d.ts
CHANGED
|
@@ -11,6 +11,24 @@ export function normalizeSessionTitle(text: any, maxLength?: number): string | n
|
|
|
11
11
|
* are skipped.
|
|
12
12
|
*/
|
|
13
13
|
export function readJsonlHeadEntries(filePath: any, maxLines?: number, maxBytes?: number): Promise<any[]>;
|
|
14
|
+
/**
|
|
15
|
+
* Parses the tail of a jsonl file (last `maxBytes`); the first, possibly
|
|
16
|
+
* partial, line is dropped unless the read started at the file's beginning.
|
|
17
|
+
*/
|
|
18
|
+
export function readJsonlTailEntries(filePath: any, maxBytes?: number): Promise<any[]>;
|
|
19
|
+
/**
|
|
20
|
+
* Session preview for the resume picker, from `{ role, text }` messages read
|
|
21
|
+
* off the head and tail of a session file: the first user message, the
|
|
22
|
+
* assistant text that answered it, and the session's last message.
|
|
23
|
+
*/
|
|
24
|
+
export function buildSessionPreview(headMessages: any, tailMessages: any): {
|
|
25
|
+
firstUserMessage: string | null;
|
|
26
|
+
firstReply: string | null;
|
|
27
|
+
lastMessage: {
|
|
28
|
+
role: any;
|
|
29
|
+
text: string | null;
|
|
30
|
+
} | null;
|
|
31
|
+
};
|
|
14
32
|
export function isExistingDirectory(targetPath: any): Promise<boolean>;
|
|
15
33
|
export function resolveSessionRunDirectory(sessionPath: any): Promise<string>;
|
|
16
34
|
export function listCandidateWorkingDirectories(options?: {}): any[];
|
package/dist/resume/shared.js
CHANGED
|
@@ -46,7 +46,7 @@ export function normalizeSessionTitle(text, maxLength = 80) {
|
|
|
46
46
|
* session files stay cheap to inspect). Unreadable files and unparsable lines
|
|
47
47
|
* are skipped.
|
|
48
48
|
*/
|
|
49
|
-
export async function readJsonlHeadEntries(filePath, maxLines =
|
|
49
|
+
export async function readJsonlHeadEntries(filePath, maxLines = 200, maxBytes = 128 * 1024) {
|
|
50
50
|
const entries = [];
|
|
51
51
|
const input = fs.createReadStream(filePath, { end: maxBytes - 1 });
|
|
52
52
|
const rl = readline.createInterface({ input, crlfDelay: Infinity });
|
|
@@ -78,6 +78,70 @@ export async function readJsonlHeadEntries(filePath, maxLines = 50, maxBytes = 1
|
|
|
78
78
|
}
|
|
79
79
|
return entries;
|
|
80
80
|
}
|
|
81
|
+
/**
|
|
82
|
+
* Parses the tail of a jsonl file (last `maxBytes`); the first, possibly
|
|
83
|
+
* partial, line is dropped unless the read started at the file's beginning.
|
|
84
|
+
*/
|
|
85
|
+
export async function readJsonlTailEntries(filePath, maxBytes = 64 * 1024) {
|
|
86
|
+
let handle;
|
|
87
|
+
try {
|
|
88
|
+
handle = await fsp.open(filePath, "r");
|
|
89
|
+
const { size } = await handle.stat();
|
|
90
|
+
const start = Math.max(0, size - maxBytes);
|
|
91
|
+
const buffer = Buffer.alloc(size - start);
|
|
92
|
+
const { bytesRead } = await handle.read(buffer, 0, buffer.length, start);
|
|
93
|
+
const lines = buffer.subarray(0, bytesRead).toString("utf8").split("\n");
|
|
94
|
+
if (start > 0) {
|
|
95
|
+
lines.shift();
|
|
96
|
+
}
|
|
97
|
+
const entries = [];
|
|
98
|
+
for (const line of lines) {
|
|
99
|
+
try {
|
|
100
|
+
if (line.trim())
|
|
101
|
+
entries.push(JSON.parse(line));
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return entries;
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
return [];
|
|
111
|
+
}
|
|
112
|
+
finally {
|
|
113
|
+
await handle?.close();
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
function clipPreviewText(text, maxLength = 2000) {
|
|
117
|
+
const trimmed = String(text || "").trim();
|
|
118
|
+
if (!trimmed) {
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
return trimmed.length > maxLength ? `${trimmed.slice(0, maxLength)}…` : trimmed;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Session preview for the resume picker, from `{ role, text }` messages read
|
|
125
|
+
* off the head and tail of a session file: the first user message, the
|
|
126
|
+
* assistant text that answered it, and the session's last message.
|
|
127
|
+
*/
|
|
128
|
+
export function buildSessionPreview(headMessages, tailMessages) {
|
|
129
|
+
const firstUserIndex = headMessages.findIndex((message) => message.role === "user");
|
|
130
|
+
const replies = [];
|
|
131
|
+
if (firstUserIndex >= 0) {
|
|
132
|
+
for (const message of headMessages.slice(firstUserIndex + 1)) {
|
|
133
|
+
if (message.role === "user")
|
|
134
|
+
break;
|
|
135
|
+
replies.push(message.text);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
const last = tailMessages[tailMessages.length - 1] || headMessages[headMessages.length - 1] || null;
|
|
139
|
+
return {
|
|
140
|
+
firstUserMessage: firstUserIndex >= 0 ? clipPreviewText(headMessages[firstUserIndex].text) : null,
|
|
141
|
+
firstReply: clipPreviewText(replies.join("\n\n")),
|
|
142
|
+
lastMessage: last ? { role: last.role, text: clipPreviewText(last.text) } : null,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
81
145
|
export async function isExistingDirectory(targetPath) {
|
|
82
146
|
const normalizedPath = typeof targetPath === "string" ? targetPath.trim() : "";
|
|
83
147
|
if (!normalizedPath) {
|
|
@@ -46,6 +46,27 @@ function injectEnableGoalsArgs(args, enableGoals) {
|
|
|
46
46
|
normalized.splice(insertAt, 0, "--enable", "goals");
|
|
47
47
|
return normalized;
|
|
48
48
|
}
|
|
49
|
+
/**
|
|
50
|
+
* Add one `-c key=value` per config override (e.g. the per-task MCP server a
|
|
51
|
+
* remote-worktree task needs), before `--listen` like `--enable goals`.
|
|
52
|
+
*
|
|
53
|
+
* @param {string[]} args
|
|
54
|
+
* @param {unknown} overrides
|
|
55
|
+
* @returns {string[]}
|
|
56
|
+
*/
|
|
57
|
+
function injectConfigOverrideArgs(args, overrides) {
|
|
58
|
+
const pairs = Array.isArray(overrides)
|
|
59
|
+
? overrides.filter((entry) => typeof entry === "string" && entry.includes("="))
|
|
60
|
+
: [];
|
|
61
|
+
if (pairs.length === 0) {
|
|
62
|
+
return args;
|
|
63
|
+
}
|
|
64
|
+
const normalized = [...args];
|
|
65
|
+
const listenIndex = normalized.indexOf("--listen");
|
|
66
|
+
const insertAt = listenIndex >= 0 ? listenIndex : normalized.length;
|
|
67
|
+
normalized.splice(insertAt, 0, ...pairs.flatMap((pair) => ["-c", pair]));
|
|
68
|
+
return normalized;
|
|
69
|
+
}
|
|
49
70
|
function createRpcError(payload) {
|
|
50
71
|
const message = String(payload?.message || payload?.error?.message || "Codex app-server request failed");
|
|
51
72
|
const error = new Error(message);
|
|
@@ -78,7 +99,7 @@ export class CodexAppServerTransport extends EventEmitter {
|
|
|
78
99
|
}
|
|
79
100
|
this.command = command;
|
|
80
101
|
this.enableGoals = options.enableGoals === true;
|
|
81
|
-
this.args = injectEnableGoalsArgs(args, this.enableGoals);
|
|
102
|
+
this.args = injectConfigOverrideArgs(injectEnableGoalsArgs(args, this.enableGoals), options.configOverrides);
|
|
82
103
|
this.env = options.env && typeof options.env === "object" ? { ...options.env } : {};
|
|
83
104
|
this.ignoreCodexApiKey = options.ignoreCodexApiKey === true;
|
|
84
105
|
this.child = null;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@love-moon/ai-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.1",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "git+https://github.com/lovemoon-ai/conductor.git"
|
|
@@ -53,12 +53,12 @@
|
|
|
53
53
|
"zod": "^4.1.5"
|
|
54
54
|
},
|
|
55
55
|
"optionalDependencies": {
|
|
56
|
-
"@love-moon/chat-web": "^0.
|
|
56
|
+
"@love-moon/chat-web": "^0.15.1"
|
|
57
57
|
},
|
|
58
58
|
"devDependencies": {
|
|
59
59
|
"@types/node": "^22.10.2",
|
|
60
60
|
"tsx": "^4.20.6",
|
|
61
61
|
"typescript": "^5.6.3"
|
|
62
62
|
},
|
|
63
|
-
"gitCommitId": "
|
|
63
|
+
"gitCommitId": "4dd9818"
|
|
64
64
|
}
|