u-foo 1.0.3 → 1.1.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/README.md +110 -11
- package/README.zh-CN.md +9 -7
- package/SKILLS/ufoo/SKILL.md +132 -0
- package/SKILLS/uinit/SKILL.md +78 -0
- package/SKILLS/ustatus/SKILL.md +36 -0
- package/bin/uclaude.js +13 -0
- package/bin/ucode-core.js +15 -0
- package/bin/ucode.js +125 -0
- package/bin/ucodex.js +13 -0
- package/bin/ufoo +9 -31
- package/bin/ufoo-assistant-agent.js +5 -0
- package/bin/ufoo-engine.js +25 -0
- package/bin/ufoo.js +17 -0
- package/modules/AGENTS.template.md +29 -11
- package/modules/bus/README.md +33 -25
- package/modules/bus/SKILLS/ubus/SKILL.md +19 -8
- package/modules/context/README.md +18 -40
- package/modules/context/SKILLS/uctx/SKILL.md +63 -1
- package/modules/online/SKILLS/ufoo-online/SKILL.md +144 -0
- package/package.json +25 -4
- package/scripts/import-pi-mono.js +124 -0
- package/scripts/postinstall.js +30 -0
- package/scripts/sync-claude-skills.sh +21 -0
- package/src/agent/cliRunner.js +554 -33
- package/src/agent/internalRunner.js +150 -56
- package/src/agent/launcher.js +754 -0
- package/src/agent/normalizeOutput.js +1 -1
- package/src/agent/notifier.js +340 -0
- package/src/agent/ptyRunner.js +847 -0
- package/src/agent/ptyWrapper.js +379 -0
- package/src/agent/readyDetector.js +175 -0
- package/src/agent/ucode.js +443 -0
- package/src/agent/ucodeBootstrap.js +113 -0
- package/src/agent/ucodeBuild.js +67 -0
- package/src/agent/ucodeDoctor.js +184 -0
- package/src/agent/ucodeRuntimeConfig.js +129 -0
- package/src/agent/ufooAgent.js +46 -42
- package/src/assistant/agent.js +260 -0
- package/src/assistant/bridge.js +172 -0
- package/src/assistant/engine.js +252 -0
- package/src/assistant/stdio.js +58 -0
- package/src/assistant/ufooEngineCli.js +306 -0
- package/src/bus/activate.js +172 -0
- package/src/bus/daemon.js +436 -0
- package/src/bus/index.js +842 -0
- package/src/bus/inject.js +315 -0
- package/src/bus/message.js +430 -0
- package/src/bus/nickname.js +88 -0
- package/src/bus/queue.js +136 -0
- package/src/bus/shake.js +26 -0
- package/src/bus/store.js +189 -0
- package/src/bus/subscriber.js +312 -0
- package/src/bus/utils.js +363 -0
- package/src/chat/agentBar.js +117 -0
- package/src/chat/agentDirectory.js +88 -0
- package/src/chat/agentSockets.js +225 -0
- package/src/chat/agentViewController.js +298 -0
- package/src/chat/chatLogController.js +115 -0
- package/src/chat/commandExecutor.js +700 -0
- package/src/chat/commands.js +132 -0
- package/src/chat/completionController.js +414 -0
- package/src/chat/cronScheduler.js +160 -0
- package/src/chat/daemonConnection.js +166 -0
- package/src/chat/daemonCoordinator.js +64 -0
- package/src/chat/daemonMessageRouter.js +257 -0
- package/src/chat/daemonReconnect.js +41 -0
- package/src/chat/daemonTransport.js +36 -0
- package/src/chat/daemonTransportDefaults.js +10 -0
- package/src/chat/dashboardKeyController.js +480 -0
- package/src/chat/dashboardView.js +154 -0
- package/src/chat/index.js +1011 -1392
- package/src/chat/inputHistoryController.js +105 -0
- package/src/chat/inputListenerController.js +304 -0
- package/src/chat/inputMath.js +104 -0
- package/src/chat/inputSubmitHandler.js +171 -0
- package/src/chat/layout.js +165 -0
- package/src/chat/pasteController.js +81 -0
- package/src/chat/rawKeyMap.js +42 -0
- package/src/chat/settingsController.js +132 -0
- package/src/chat/statusLineController.js +177 -0
- package/src/chat/streamTracker.js +138 -0
- package/src/chat/text.js +70 -0
- package/src/chat/transport.js +61 -0
- package/src/cli/busCoreCommands.js +59 -0
- package/src/cli/ctxCoreCommands.js +199 -0
- package/src/cli/onlineCoreCommands.js +379 -0
- package/src/cli.js +1162 -96
- package/src/code/README.md +29 -0
- package/src/code/UCODE_PROMPT.md +32 -0
- package/src/code/agent.js +1651 -0
- package/src/code/cli.js +158 -0
- package/src/code/config +0 -0
- package/src/code/dispatch.js +42 -0
- package/src/code/index.js +70 -0
- package/src/code/nativeRunner.js +1213 -0
- package/src/code/runtime.js +154 -0
- package/src/code/sessionStore.js +162 -0
- package/src/code/taskDecomposer.js +269 -0
- package/src/code/tools/bash.js +53 -0
- package/src/code/tools/common.js +42 -0
- package/src/code/tools/edit.js +70 -0
- package/src/code/tools/read.js +44 -0
- package/src/code/tools/write.js +35 -0
- package/src/code/tui.js +1580 -0
- package/src/config.js +56 -3
- package/src/context/decisions.js +324 -0
- package/src/context/doctor.js +183 -0
- package/src/context/index.js +55 -0
- package/src/context/sync.js +127 -0
- package/src/daemon/agentProcessManager.js +74 -0
- package/src/daemon/cronOps.js +241 -0
- package/src/daemon/index.js +998 -170
- package/src/daemon/ipcServer.js +99 -0
- package/src/daemon/ops.js +630 -48
- package/src/daemon/promptLoop.js +319 -0
- package/src/daemon/promptRequest.js +101 -0
- package/src/daemon/providerSessions.js +306 -0
- package/src/daemon/reporting.js +90 -0
- package/src/daemon/run.js +31 -1
- package/src/daemon/status.js +48 -8
- package/src/doctor/index.js +50 -0
- package/src/init/index.js +318 -0
- package/src/online/bridge.js +663 -0
- package/src/online/client.js +245 -0
- package/src/online/runner.js +253 -0
- package/src/online/server.js +992 -0
- package/src/online/tokens.js +103 -0
- package/src/report/store.js +331 -0
- package/src/shared/eventContract.js +35 -0
- package/src/shared/ptySocketContract.js +21 -0
- package/src/skills/index.js +159 -0
- package/src/status/index.js +285 -0
- package/src/terminal/adapterContract.js +87 -0
- package/src/terminal/adapterRouter.js +84 -0
- package/src/terminal/adapters/externalAdapter.js +14 -0
- package/src/terminal/adapters/internalAdapter.js +13 -0
- package/src/terminal/adapters/internalPtyAdapter.js +42 -0
- package/src/terminal/adapters/internalQueueAdapter.js +37 -0
- package/src/terminal/adapters/terminalAdapter.js +31 -0
- package/src/terminal/adapters/tmuxAdapter.js +30 -0
- package/src/terminal/detect.js +64 -0
- package/src/terminal/index.js +8 -0
- package/src/terminal/iterm2.js +126 -0
- package/src/ufoo/agentsStore.js +107 -0
- package/src/ufoo/paths.js +46 -0
- package/src/utils/banner.js +76 -0
- package/bin/uclaude +0 -65
- package/bin/ucodex +0 -65
- package/modules/bus/scripts/bus-alert.sh +0 -185
- package/modules/bus/scripts/bus-listen.sh +0 -117
- package/modules/context/ASSUMPTIONS.md +0 -7
- package/modules/context/CONSTRAINTS.md +0 -7
- package/modules/context/CONTEXT-STRUCTURE.md +0 -49
- package/modules/context/DECISION-PROTOCOL.md +0 -62
- package/modules/context/HANDOFF.md +0 -33
- package/modules/context/RULES.md +0 -15
- package/modules/context/SKILLS/README.md +0 -14
- package/modules/context/SYSTEM.md +0 -18
- package/modules/context/TEMPLATES/assumptions.md +0 -4
- package/modules/context/TEMPLATES/constraints.md +0 -4
- package/modules/context/TEMPLATES/decision.md +0 -16
- package/modules/context/TEMPLATES/project-context-readme.md +0 -6
- package/modules/context/TEMPLATES/system.md +0 -3
- package/modules/context/TEMPLATES/terminology.md +0 -4
- package/modules/context/TERMINOLOGY.md +0 -10
- package/scripts/banner.sh +0 -89
- package/scripts/bus-alert.sh +0 -6
- package/scripts/bus-autotrigger.sh +0 -6
- package/scripts/bus-daemon.sh +0 -231
- package/scripts/bus-inject.sh +0 -144
- package/scripts/bus-listen.sh +0 -6
- package/scripts/bus.sh +0 -984
- package/scripts/context-decisions.sh +0 -167
- package/scripts/context-doctor.sh +0 -72
- package/scripts/context-lint.sh +0 -110
- package/scripts/doctor.sh +0 -22
- package/scripts/init.sh +0 -247
- package/scripts/skills.sh +0 -113
- package/scripts/status.sh +0 -125
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
const { runCliAgent } = require("../agent/cliRunner");
|
|
4
|
+
const { normalizeCliOutput } = require("../agent/normalizeOutput");
|
|
5
|
+
const { resolveAssistantEngine, runExternalAssistantEngine } = require("./engine");
|
|
6
|
+
const { getUfooPaths } = require("../ufoo/paths");
|
|
7
|
+
|
|
8
|
+
const ASSISTANT_JSON_SCHEMA = {
|
|
9
|
+
type: "object",
|
|
10
|
+
properties: {
|
|
11
|
+
ok: { type: "boolean" },
|
|
12
|
+
summary: { type: "string" },
|
|
13
|
+
artifacts: {
|
|
14
|
+
type: "array",
|
|
15
|
+
items: { type: "string" },
|
|
16
|
+
},
|
|
17
|
+
logs: {
|
|
18
|
+
type: "array",
|
|
19
|
+
items: { type: "string" },
|
|
20
|
+
},
|
|
21
|
+
error: { type: "string" },
|
|
22
|
+
metrics: {
|
|
23
|
+
type: "object",
|
|
24
|
+
additionalProperties: true,
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
required: ["ok", "summary"],
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
function parseTaskPayload(payload = {}) {
|
|
31
|
+
const projectRoot = typeof payload.project_root === "string" ? payload.project_root : process.cwd();
|
|
32
|
+
const provider = typeof payload.provider === "string" ? payload.provider : "";
|
|
33
|
+
const fallbackProvider = typeof payload.fallback_provider === "string" ? payload.fallback_provider : "";
|
|
34
|
+
const model = typeof payload.model === "string" ? payload.model : "";
|
|
35
|
+
const task = typeof payload.task === "string" ? payload.task.trim() : "";
|
|
36
|
+
const kind = typeof payload.kind === "string" && payload.kind ? payload.kind : "mixed";
|
|
37
|
+
const context = typeof payload.context === "string" ? payload.context : "";
|
|
38
|
+
const expectText = typeof payload.expect === "string" ? payload.expect : "";
|
|
39
|
+
const timeoutMs = Number.isFinite(payload.timeout_ms) ? payload.timeout_ms : 60000;
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
projectRoot,
|
|
43
|
+
provider,
|
|
44
|
+
fallbackProvider,
|
|
45
|
+
model,
|
|
46
|
+
task,
|
|
47
|
+
kind,
|
|
48
|
+
context,
|
|
49
|
+
expect: expectText,
|
|
50
|
+
timeoutMs,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function normalizeAssistantPayload(parsed, fallbackError = "") {
|
|
55
|
+
if (!parsed || typeof parsed !== "object") {
|
|
56
|
+
const text = String(parsed || "").trim();
|
|
57
|
+
if (text) {
|
|
58
|
+
return {
|
|
59
|
+
ok: true,
|
|
60
|
+
summary: text,
|
|
61
|
+
artifacts: [],
|
|
62
|
+
logs: [],
|
|
63
|
+
error: "",
|
|
64
|
+
metrics: {},
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
return {
|
|
68
|
+
ok: false,
|
|
69
|
+
summary: "",
|
|
70
|
+
artifacts: [],
|
|
71
|
+
logs: [],
|
|
72
|
+
error: fallbackError || "assistant returned invalid payload",
|
|
73
|
+
metrics: {},
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
ok: parsed.ok !== false,
|
|
79
|
+
summary: typeof parsed.summary === "string" ? parsed.summary : "",
|
|
80
|
+
artifacts: Array.isArray(parsed.artifacts) ? parsed.artifacts : [],
|
|
81
|
+
logs: Array.isArray(parsed.logs) ? parsed.logs : [],
|
|
82
|
+
error: typeof parsed.error === "string" ? parsed.error : "",
|
|
83
|
+
metrics: parsed.metrics && typeof parsed.metrics === "object" ? parsed.metrics : {},
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function buildAssistantSystemPrompt(taskInput) {
|
|
88
|
+
return [
|
|
89
|
+
"You are ufoo-assistant-agent, a private helper for ufoo-agent.",
|
|
90
|
+
"You are NOT exposed on the event bus.",
|
|
91
|
+
"Execute the requested task using local project context and shell/tool access as needed.",
|
|
92
|
+
"Return ONLY JSON that matches schema: {ok, summary, artifacts, logs, error, metrics}.",
|
|
93
|
+
"Rules:",
|
|
94
|
+
"- summary: concise factual result for ufoo-agent to consume.",
|
|
95
|
+
"- artifacts: key files/commands/findings (short strings).",
|
|
96
|
+
"- logs: optional concise trace points.",
|
|
97
|
+
"- error: non-empty only when ok=false.",
|
|
98
|
+
"- Do not include markdown or prose outside JSON.",
|
|
99
|
+
"",
|
|
100
|
+
"Task input:",
|
|
101
|
+
JSON.stringify(taskInput),
|
|
102
|
+
].join("\n");
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function getAssistantStatePaths(projectRoot) {
|
|
106
|
+
const dir = getUfooPaths(projectRoot).agentDir;
|
|
107
|
+
return {
|
|
108
|
+
sessionDir: path.join(dir, "sessions"),
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function getAssistantSessionStateFile(projectRoot, engine = "assistant") {
|
|
113
|
+
const { sessionDir } = getAssistantStatePaths(projectRoot);
|
|
114
|
+
return path.join(sessionDir, `ufoo-assistant-${engine}.json`);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function loadAssistantState(projectRoot, engine = "assistant") {
|
|
118
|
+
const file = getAssistantSessionStateFile(projectRoot, engine);
|
|
119
|
+
try {
|
|
120
|
+
return JSON.parse(fs.readFileSync(file, "utf8"));
|
|
121
|
+
} catch {
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function saveAssistantState(projectRoot, engine = "assistant", state = {}) {
|
|
127
|
+
const { sessionDir } = getAssistantStatePaths(projectRoot);
|
|
128
|
+
fs.mkdirSync(sessionDir, { recursive: true });
|
|
129
|
+
const file = getAssistantSessionStateFile(projectRoot, engine);
|
|
130
|
+
fs.writeFileSync(file, JSON.stringify(state, null, 2));
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function isSessionError(errorText = "") {
|
|
134
|
+
const text = String(errorText || "").toLowerCase();
|
|
135
|
+
return text.includes("session id")
|
|
136
|
+
|| text.includes("session-id")
|
|
137
|
+
|| text.includes("already in use");
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async function runAssistantAgentTask(payload = {}) {
|
|
141
|
+
const taskInput = parseTaskPayload(payload);
|
|
142
|
+
const startedAt = Date.now();
|
|
143
|
+
|
|
144
|
+
if (!taskInput.task) {
|
|
145
|
+
return {
|
|
146
|
+
ok: false,
|
|
147
|
+
summary: "",
|
|
148
|
+
artifacts: [],
|
|
149
|
+
logs: [],
|
|
150
|
+
error: "missing task",
|
|
151
|
+
metrics: { duration_ms: Date.now() - startedAt },
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const systemPrompt = buildAssistantSystemPrompt(taskInput);
|
|
156
|
+
const engine = resolveAssistantEngine({
|
|
157
|
+
projectRoot: taskInput.projectRoot,
|
|
158
|
+
requestedProvider: taskInput.provider,
|
|
159
|
+
requestedModel: taskInput.model,
|
|
160
|
+
fallbackProvider: taskInput.fallbackProvider,
|
|
161
|
+
});
|
|
162
|
+
const assistantState = loadAssistantState(taskInput.projectRoot, engine.engine);
|
|
163
|
+
|
|
164
|
+
let cliRes = null;
|
|
165
|
+
if (engine.kind === "external") {
|
|
166
|
+
cliRes = await runExternalAssistantEngine({
|
|
167
|
+
engine,
|
|
168
|
+
timeoutMs: taskInput.timeoutMs,
|
|
169
|
+
payload: {
|
|
170
|
+
request_type: "assistant_task",
|
|
171
|
+
schema_version: 1,
|
|
172
|
+
engine: engine.engine,
|
|
173
|
+
project_root: taskInput.projectRoot,
|
|
174
|
+
task: taskInput.task,
|
|
175
|
+
kind: taskInput.kind,
|
|
176
|
+
context: taskInput.context,
|
|
177
|
+
expect: taskInput.expect,
|
|
178
|
+
model: engine.model || "",
|
|
179
|
+
session_id: assistantState && typeof assistantState.sessionId === "string" ? assistantState.sessionId : "",
|
|
180
|
+
timeout_ms: taskInput.timeoutMs,
|
|
181
|
+
},
|
|
182
|
+
});
|
|
183
|
+
} else {
|
|
184
|
+
const runCli = async (sessionId) => runCliAgent({
|
|
185
|
+
provider: engine.provider,
|
|
186
|
+
model: engine.model,
|
|
187
|
+
prompt: taskInput.task,
|
|
188
|
+
systemPrompt,
|
|
189
|
+
jsonSchema: ASSISTANT_JSON_SCHEMA,
|
|
190
|
+
disableSession: false,
|
|
191
|
+
sessionId,
|
|
192
|
+
cwd: taskInput.projectRoot,
|
|
193
|
+
timeoutMs: taskInput.timeoutMs,
|
|
194
|
+
sandbox: taskInput.kind === "explore" ? "read-only" : "workspace-write",
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
const preferredSession = assistantState && typeof assistantState.sessionId === "string"
|
|
198
|
+
? assistantState.sessionId
|
|
199
|
+
: undefined;
|
|
200
|
+
cliRes = await runCli(preferredSession);
|
|
201
|
+
if (!cliRes.ok && preferredSession && isSessionError(cliRes.error)) {
|
|
202
|
+
cliRes = await runCli(undefined);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (!cliRes || cliRes.ok === false) {
|
|
207
|
+
return {
|
|
208
|
+
ok: false,
|
|
209
|
+
summary: "",
|
|
210
|
+
artifacts: [],
|
|
211
|
+
logs: [],
|
|
212
|
+
error: (cliRes && cliRes.error) || "assistant cli failed",
|
|
213
|
+
metrics: { duration_ms: Date.now() - startedAt },
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
let result;
|
|
218
|
+
if (engine.kind === "external") {
|
|
219
|
+
result = normalizeAssistantPayload(cliRes);
|
|
220
|
+
} else {
|
|
221
|
+
const normalized = normalizeCliOutput(cliRes.output);
|
|
222
|
+
let parsed;
|
|
223
|
+
try {
|
|
224
|
+
parsed = JSON.parse(normalized);
|
|
225
|
+
} catch {
|
|
226
|
+
parsed = normalized;
|
|
227
|
+
}
|
|
228
|
+
result = normalizeAssistantPayload(parsed);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
result.metrics = {
|
|
232
|
+
...result.metrics,
|
|
233
|
+
duration_ms: Date.now() - startedAt,
|
|
234
|
+
};
|
|
235
|
+
if (!result.ok && !result.error) {
|
|
236
|
+
result.error = "assistant task failed";
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
saveAssistantState(taskInput.projectRoot, engine.engine, {
|
|
240
|
+
engine: engine.engine,
|
|
241
|
+
provider: engine.provider || "",
|
|
242
|
+
model: engine.model || "",
|
|
243
|
+
sessionId: cliRes && typeof cliRes.sessionId === "string" ? cliRes.sessionId : "",
|
|
244
|
+
updated_at: new Date().toISOString(),
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
return result;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
module.exports = {
|
|
251
|
+
runAssistantAgentTask,
|
|
252
|
+
parseTaskPayload,
|
|
253
|
+
normalizeAssistantPayload,
|
|
254
|
+
buildAssistantSystemPrompt,
|
|
255
|
+
getAssistantSessionStateFile,
|
|
256
|
+
loadAssistantState,
|
|
257
|
+
saveAssistantState,
|
|
258
|
+
isSessionError,
|
|
259
|
+
ASSISTANT_JSON_SCHEMA,
|
|
260
|
+
};
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
const { spawn } = require("child_process");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
|
|
4
|
+
function resolveAssistantCommand() {
|
|
5
|
+
const raw = String(process.env.UFOO_ASSISTANT_CMD || "ufoo-assistant-agent").trim();
|
|
6
|
+
if (!raw || raw === "ufoo-assistant-agent") {
|
|
7
|
+
return {
|
|
8
|
+
command: process.execPath,
|
|
9
|
+
args: [path.resolve(__dirname, "../../bin/ufoo-assistant-agent.js")],
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
const parts = raw.split(/\s+/).filter(Boolean);
|
|
13
|
+
if (parts.length === 0) {
|
|
14
|
+
return {
|
|
15
|
+
command: process.execPath,
|
|
16
|
+
args: [path.resolve(__dirname, "../../bin/ufoo-assistant-agent.js")],
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
return { command: parts[0], args: parts.slice(1) };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function parseAssistantOutput(stdout) {
|
|
23
|
+
const text = String(stdout || "").trim();
|
|
24
|
+
if (!text) return null;
|
|
25
|
+
try {
|
|
26
|
+
return JSON.parse(text);
|
|
27
|
+
} catch {
|
|
28
|
+
// Continue to line-based fallback.
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const lines = text.split(/\r?\n/).filter(Boolean);
|
|
32
|
+
for (let i = lines.length - 1; i >= 0; i -= 1) {
|
|
33
|
+
try {
|
|
34
|
+
return JSON.parse(lines[i]);
|
|
35
|
+
} catch {
|
|
36
|
+
// ignore malformed line
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function normalizeResponse(parsed, fallbackError = "") {
|
|
43
|
+
if (!parsed || typeof parsed !== "object") {
|
|
44
|
+
return {
|
|
45
|
+
ok: false,
|
|
46
|
+
summary: "",
|
|
47
|
+
artifacts: [],
|
|
48
|
+
logs: [],
|
|
49
|
+
error: fallbackError || "assistant returned invalid JSON",
|
|
50
|
+
metrics: {},
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
ok: parsed.ok !== false,
|
|
56
|
+
summary: typeof parsed.summary === "string" ? parsed.summary : "",
|
|
57
|
+
artifacts: Array.isArray(parsed.artifacts) ? parsed.artifacts : [],
|
|
58
|
+
logs: Array.isArray(parsed.logs) ? parsed.logs : [],
|
|
59
|
+
error: typeof parsed.error === "string" ? parsed.error : "",
|
|
60
|
+
metrics: parsed.metrics && typeof parsed.metrics === "object" ? parsed.metrics : {},
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function runAssistantTask({
|
|
65
|
+
projectRoot,
|
|
66
|
+
provider = "",
|
|
67
|
+
fallbackProvider = "",
|
|
68
|
+
model = "",
|
|
69
|
+
task = "",
|
|
70
|
+
kind = "mixed",
|
|
71
|
+
context = "",
|
|
72
|
+
expect = "",
|
|
73
|
+
timeoutMs = 60000,
|
|
74
|
+
} = {}) {
|
|
75
|
+
return new Promise((resolve) => {
|
|
76
|
+
const startedAt = Date.now();
|
|
77
|
+
const { command, args } = resolveAssistantCommand();
|
|
78
|
+
const payload = {
|
|
79
|
+
request_id: `assistant-${startedAt}`,
|
|
80
|
+
project_root: projectRoot,
|
|
81
|
+
provider,
|
|
82
|
+
fallback_provider: fallbackProvider,
|
|
83
|
+
model,
|
|
84
|
+
task,
|
|
85
|
+
kind,
|
|
86
|
+
context,
|
|
87
|
+
expect,
|
|
88
|
+
timeout_ms: timeoutMs,
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const child = spawn(command, args, {
|
|
92
|
+
cwd: projectRoot,
|
|
93
|
+
env: { ...process.env, UFOO_ASSISTANT_MODE: "private" },
|
|
94
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
let stdout = "";
|
|
98
|
+
let stderr = "";
|
|
99
|
+
let settled = false;
|
|
100
|
+
const finish = (result) => {
|
|
101
|
+
if (settled) return;
|
|
102
|
+
settled = true;
|
|
103
|
+
resolve(result);
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const timer = setTimeout(() => {
|
|
107
|
+
try {
|
|
108
|
+
child.kill("SIGTERM");
|
|
109
|
+
} catch {
|
|
110
|
+
// ignore
|
|
111
|
+
}
|
|
112
|
+
finish({
|
|
113
|
+
ok: false,
|
|
114
|
+
summary: "",
|
|
115
|
+
artifacts: [],
|
|
116
|
+
logs: [],
|
|
117
|
+
error: "assistant timeout",
|
|
118
|
+
metrics: { duration_ms: Date.now() - startedAt },
|
|
119
|
+
});
|
|
120
|
+
}, timeoutMs);
|
|
121
|
+
|
|
122
|
+
child.on("error", (err) => {
|
|
123
|
+
clearTimeout(timer);
|
|
124
|
+
finish({
|
|
125
|
+
ok: false,
|
|
126
|
+
summary: "",
|
|
127
|
+
artifacts: [],
|
|
128
|
+
logs: [],
|
|
129
|
+
error: err && err.message ? err.message : "assistant spawn failed",
|
|
130
|
+
metrics: { duration_ms: Date.now() - startedAt },
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
child.stdout.on("data", (chunk) => {
|
|
135
|
+
stdout += chunk.toString("utf8");
|
|
136
|
+
});
|
|
137
|
+
child.stderr.on("data", (chunk) => {
|
|
138
|
+
stderr += chunk.toString("utf8");
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
child.on("close", (code) => {
|
|
142
|
+
clearTimeout(timer);
|
|
143
|
+
const parsed = parseAssistantOutput(stdout);
|
|
144
|
+
const fallbackError = code === 0
|
|
145
|
+
? ""
|
|
146
|
+
: (stderr || `assistant exited with code ${code}`);
|
|
147
|
+
const normalized = normalizeResponse(parsed, fallbackError);
|
|
148
|
+
normalized.metrics = {
|
|
149
|
+
...normalized.metrics,
|
|
150
|
+
duration_ms: Date.now() - startedAt,
|
|
151
|
+
};
|
|
152
|
+
if (!normalized.ok && !normalized.error) {
|
|
153
|
+
normalized.error = fallbackError || "assistant failed";
|
|
154
|
+
}
|
|
155
|
+
finish(normalized);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
try {
|
|
159
|
+
child.stdin.write(`${JSON.stringify(payload)}\n`);
|
|
160
|
+
child.stdin.end();
|
|
161
|
+
} catch {
|
|
162
|
+
// stdin may already be closed.
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
module.exports = {
|
|
168
|
+
runAssistantTask,
|
|
169
|
+
parseAssistantOutput,
|
|
170
|
+
normalizeResponse,
|
|
171
|
+
resolveAssistantCommand,
|
|
172
|
+
};
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
const { spawn } = require("child_process");
|
|
2
|
+
const { loadConfig, normalizeAssistantEngine } = require("../config");
|
|
3
|
+
|
|
4
|
+
function splitCommand(raw, fallback = "ufoo-engine") {
|
|
5
|
+
const text = String(raw || "").trim();
|
|
6
|
+
if (!text) return { command: fallback, args: [] };
|
|
7
|
+
const parts = text.split(/\s+/).filter(Boolean);
|
|
8
|
+
if (parts.length === 0) return { command: fallback, args: [] };
|
|
9
|
+
return { command: parts[0], args: parts.slice(1) };
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function resolveAssistantEngine({
|
|
13
|
+
projectRoot,
|
|
14
|
+
requestedProvider = "",
|
|
15
|
+
requestedModel = "",
|
|
16
|
+
fallbackProvider = "",
|
|
17
|
+
} = {}) {
|
|
18
|
+
const config = loadConfig(projectRoot);
|
|
19
|
+
|
|
20
|
+
const hasRequestedProvider = String(requestedProvider || "").trim().length > 0;
|
|
21
|
+
const requested = normalizeAssistantEngine(requestedProvider);
|
|
22
|
+
const configEngine = normalizeAssistantEngine(config.assistantEngine);
|
|
23
|
+
const fallback = normalizeAssistantEngine(fallbackProvider) || "codex";
|
|
24
|
+
|
|
25
|
+
let selected = requested;
|
|
26
|
+
if (selected === "auto") {
|
|
27
|
+
// Explicit assistant_call provider=auto should inherit current main agent provider.
|
|
28
|
+
selected = hasRequestedProvider ? fallback : configEngine;
|
|
29
|
+
}
|
|
30
|
+
if (selected === "auto") selected = fallback;
|
|
31
|
+
if (selected === "auto") selected = "codex";
|
|
32
|
+
|
|
33
|
+
const model =
|
|
34
|
+
String(requestedModel || "").trim()
|
|
35
|
+
|| String(process.env.UFOO_ASSISTANT_MODEL || "").trim()
|
|
36
|
+
|| String(config.assistantModel || "").trim()
|
|
37
|
+
|| "";
|
|
38
|
+
|
|
39
|
+
if (selected === "claude") {
|
|
40
|
+
return {
|
|
41
|
+
engine: "claude",
|
|
42
|
+
kind: "cli",
|
|
43
|
+
provider: "claude-cli",
|
|
44
|
+
model,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (selected === "ufoo") {
|
|
49
|
+
const { command, args } = splitCommand(
|
|
50
|
+
process.env.UFOO_ASSISTANT_UFOO_CMD || config.assistantUfooCmd,
|
|
51
|
+
"ufoo-engine"
|
|
52
|
+
);
|
|
53
|
+
return {
|
|
54
|
+
engine: "ufoo",
|
|
55
|
+
kind: "external",
|
|
56
|
+
command,
|
|
57
|
+
args,
|
|
58
|
+
model,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return {
|
|
63
|
+
engine: "codex",
|
|
64
|
+
kind: "cli",
|
|
65
|
+
provider: "codex-cli",
|
|
66
|
+
model,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function parseEngineJson(stdout) {
|
|
71
|
+
const text = String(stdout || "").trim();
|
|
72
|
+
if (!text) return null;
|
|
73
|
+
try {
|
|
74
|
+
return JSON.parse(text);
|
|
75
|
+
} catch {
|
|
76
|
+
// continue to line fallback
|
|
77
|
+
}
|
|
78
|
+
const lines = text.split(/\r?\n/).filter(Boolean);
|
|
79
|
+
for (let i = lines.length - 1; i >= 0; i -= 1) {
|
|
80
|
+
try {
|
|
81
|
+
return JSON.parse(lines[i]);
|
|
82
|
+
} catch {
|
|
83
|
+
// ignore bad lines
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function isUnsupportedArgError(errText) {
|
|
90
|
+
const text = String(errText || "").toLowerCase();
|
|
91
|
+
return text.includes("unknown option")
|
|
92
|
+
|| text.includes("unknown argument")
|
|
93
|
+
|| text.includes("unexpected argument")
|
|
94
|
+
|| text.includes("unrecognized option");
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function buildExternalEngineArgs(engine = {}, payload = {}) {
|
|
98
|
+
const args = Array.isArray(engine.args) ? [...engine.args] : [];
|
|
99
|
+
args.push("--assistant-task", "--json");
|
|
100
|
+
if (payload.model) args.push("--model", String(payload.model));
|
|
101
|
+
if (payload.session_id) args.push("--session-id", String(payload.session_id));
|
|
102
|
+
if (payload.project_root) args.push("--cwd", String(payload.project_root));
|
|
103
|
+
if (payload.kind) args.push("--kind", String(payload.kind));
|
|
104
|
+
if (payload.context) args.push("--context", String(payload.context));
|
|
105
|
+
if (payload.expect) args.push("--expect", String(payload.expect));
|
|
106
|
+
args.push(String(payload.task || ""));
|
|
107
|
+
return args;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function extractSessionId(parsed) {
|
|
111
|
+
if (!parsed || typeof parsed !== "object") return "";
|
|
112
|
+
return String(parsed.session_id || parsed.sessionId || parsed.session || "").trim();
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async function runExternalAssistantEngine({
|
|
116
|
+
engine,
|
|
117
|
+
payload,
|
|
118
|
+
timeoutMs = 60000,
|
|
119
|
+
}) {
|
|
120
|
+
const startedAt = Date.now();
|
|
121
|
+
|
|
122
|
+
const runAttempt = (attempt = {}) => new Promise((resolve) => {
|
|
123
|
+
const child = spawn(engine.command, attempt.args || [], {
|
|
124
|
+
cwd: payload.project_root || process.cwd(),
|
|
125
|
+
env: { ...process.env, UFOO_ASSISTANT_ENGINE: engine.engine || "ufoo" },
|
|
126
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
let stdout = "";
|
|
130
|
+
let stderr = "";
|
|
131
|
+
let settled = false;
|
|
132
|
+
const finish = (result) => {
|
|
133
|
+
if (settled) return;
|
|
134
|
+
settled = true;
|
|
135
|
+
resolve(result);
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
const timer = setTimeout(() => {
|
|
139
|
+
try {
|
|
140
|
+
child.kill("SIGTERM");
|
|
141
|
+
} catch {
|
|
142
|
+
// ignore
|
|
143
|
+
}
|
|
144
|
+
finish({
|
|
145
|
+
ok: false,
|
|
146
|
+
mode: attempt.mode,
|
|
147
|
+
code: -1,
|
|
148
|
+
stdout,
|
|
149
|
+
stderr,
|
|
150
|
+
error: "assistant engine timeout",
|
|
151
|
+
});
|
|
152
|
+
}, timeoutMs);
|
|
153
|
+
|
|
154
|
+
child.on("error", (err) => {
|
|
155
|
+
clearTimeout(timer);
|
|
156
|
+
finish({
|
|
157
|
+
ok: false,
|
|
158
|
+
mode: attempt.mode,
|
|
159
|
+
code: -1,
|
|
160
|
+
stdout,
|
|
161
|
+
stderr,
|
|
162
|
+
error: err && err.message ? err.message : "assistant engine spawn failed",
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
child.stdout.on("data", (chunk) => {
|
|
167
|
+
stdout += chunk.toString("utf8");
|
|
168
|
+
});
|
|
169
|
+
child.stderr.on("data", (chunk) => {
|
|
170
|
+
stderr += chunk.toString("utf8");
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
child.on("close", (code) => {
|
|
174
|
+
clearTimeout(timer);
|
|
175
|
+
finish({
|
|
176
|
+
ok: code === 0,
|
|
177
|
+
mode: attempt.mode,
|
|
178
|
+
code,
|
|
179
|
+
stdout,
|
|
180
|
+
stderr,
|
|
181
|
+
error: "",
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
try {
|
|
186
|
+
if (attempt.input) child.stdin.write(attempt.input);
|
|
187
|
+
child.stdin.end();
|
|
188
|
+
} catch {
|
|
189
|
+
// ignore stdin errors
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
const argsAttempt = {
|
|
194
|
+
mode: "args",
|
|
195
|
+
args: buildExternalEngineArgs(engine, payload),
|
|
196
|
+
input: "",
|
|
197
|
+
};
|
|
198
|
+
let result = await runAttempt(argsAttempt);
|
|
199
|
+
|
|
200
|
+
if (!result.ok && isUnsupportedArgError(result.stderr || result.stdout || result.error)) {
|
|
201
|
+
result = await runAttempt({
|
|
202
|
+
mode: "stdin-json",
|
|
203
|
+
args: Array.isArray(engine.args) ? [...engine.args] : [],
|
|
204
|
+
input: `${JSON.stringify(payload)}\n`,
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const parsed = parseEngineJson(result.stdout);
|
|
209
|
+
if (parsed && typeof parsed === "object") {
|
|
210
|
+
return {
|
|
211
|
+
...parsed,
|
|
212
|
+
sessionId: extractSessionId(parsed),
|
|
213
|
+
metrics: {
|
|
214
|
+
...(parsed.metrics && typeof parsed.metrics === "object" ? parsed.metrics : {}),
|
|
215
|
+
duration_ms: Date.now() - startedAt,
|
|
216
|
+
},
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (result.ok) {
|
|
221
|
+
const summary = String(result.stdout || "").trim();
|
|
222
|
+
return {
|
|
223
|
+
ok: true,
|
|
224
|
+
summary,
|
|
225
|
+
artifacts: [],
|
|
226
|
+
logs: [],
|
|
227
|
+
error: "",
|
|
228
|
+
sessionId: "",
|
|
229
|
+
metrics: { duration_ms: Date.now() - startedAt },
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return {
|
|
234
|
+
ok: false,
|
|
235
|
+
summary: "",
|
|
236
|
+
artifacts: [],
|
|
237
|
+
logs: [],
|
|
238
|
+
error: String(result.stderr || result.stdout || result.error || `assistant engine exited with code ${result.code}`).trim(),
|
|
239
|
+
sessionId: "",
|
|
240
|
+
metrics: { duration_ms: Date.now() - startedAt },
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
module.exports = {
|
|
245
|
+
resolveAssistantEngine,
|
|
246
|
+
runExternalAssistantEngine,
|
|
247
|
+
parseEngineJson,
|
|
248
|
+
splitCommand,
|
|
249
|
+
buildExternalEngineArgs,
|
|
250
|
+
isUnsupportedArgError,
|
|
251
|
+
extractSessionId,
|
|
252
|
+
};
|