helloloop 0.2.1 → 0.3.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/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/README.md +228 -279
- package/hosts/claude/marketplace/plugins/helloloop/.claude-plugin/plugin.json +1 -1
- package/hosts/claude/marketplace/plugins/helloloop/commands/helloloop.md +16 -9
- package/hosts/claude/marketplace/plugins/helloloop/skills/helloloop/SKILL.md +4 -1
- package/hosts/gemini/extension/GEMINI.md +8 -4
- package/hosts/gemini/extension/commands/helloloop.toml +15 -8
- package/hosts/gemini/extension/gemini-extension.json +1 -1
- package/package.json +2 -2
- package/scripts/uninstall-home-plugin.ps1 +25 -0
- package/skills/helloloop/SKILL.md +28 -3
- package/src/analyze_confirmation.mjs +79 -3
- package/src/analyze_prompt.mjs +12 -0
- package/src/analyze_user_input.mjs +303 -0
- package/src/analyzer.mjs +38 -0
- package/src/cli.mjs +211 -25
- package/src/cli_support.mjs +90 -23
- package/src/discovery.mjs +243 -9
- package/src/discovery_inference.mjs +62 -18
- package/src/discovery_paths.mjs +143 -8
- package/src/discovery_prompt.mjs +298 -0
- package/src/install.mjs +153 -0
- package/src/rebuild.mjs +116 -0
- package/templates/analysis-output.schema.json +58 -0
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
|
|
3
|
+
import { classifyExplicitPath, pathExists, resolveAbsolute } from "./discovery_paths.mjs";
|
|
4
|
+
|
|
5
|
+
const DOC_SUFFIX = new Set([
|
|
6
|
+
".md",
|
|
7
|
+
".markdown",
|
|
8
|
+
".mdx",
|
|
9
|
+
".txt",
|
|
10
|
+
".rst",
|
|
11
|
+
".adoc",
|
|
12
|
+
]);
|
|
13
|
+
|
|
14
|
+
const ROLE_LABELS = {
|
|
15
|
+
docs: "开发文档",
|
|
16
|
+
repo: "项目路径",
|
|
17
|
+
input: "补充路径",
|
|
18
|
+
new_repo: "项目路径",
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const SOURCE_LABELS = {
|
|
22
|
+
flag: "命令参数",
|
|
23
|
+
positional: "命令附带路径",
|
|
24
|
+
interactive: "交互确认",
|
|
25
|
+
interactive_new_repo: "交互确认",
|
|
26
|
+
workspace_single_doc: "工作区唯一文档候选",
|
|
27
|
+
workspace_single_repo: "工作区唯一项目候选",
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
function isProbablyPathToken(token) {
|
|
31
|
+
const value = String(token || "").trim();
|
|
32
|
+
if (!value) {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (/^[A-Za-z]:[\\/]/.test(value) || /^\\\\/.test(value)) {
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (/^\.\.?([\\/]|$)/.test(value) || /^[~][\\/]/.test(value)) {
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (value.includes("/") || value.includes("\\")) {
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return DOC_SUFFIX.has(path.extname(value).toLowerCase());
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function normalizePathKey(targetPath) {
|
|
52
|
+
const resolved = path.resolve(String(targetPath || ""));
|
|
53
|
+
return process.platform === "win32" ? resolved.toLowerCase() : resolved;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function isSamePath(left, right) {
|
|
57
|
+
return normalizePathKey(left) === normalizePathKey(right);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function createIssue(severity, message) {
|
|
61
|
+
return { severity, message: String(message || "").trim() };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function roleLabel(role) {
|
|
65
|
+
return ROLE_LABELS[role] || "路径";
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function sourceLabel(source) {
|
|
69
|
+
return SOURCE_LABELS[source] || "输入";
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function formatPathRef(ref) {
|
|
73
|
+
const label = roleLabel(ref.role);
|
|
74
|
+
const statusSuffix = ref.role === "new_repo" ? ",当前不存在,将按新项目创建" : "";
|
|
75
|
+
const suffix = ref.source ? `(来源:${sourceLabel(ref.source)}${statusSuffix})` : "";
|
|
76
|
+
return `${label}:${String(ref.absolutePath || "").replaceAll("\\", "/")}${suffix}`;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function ensureSelectionSource(selectionSources, role, source) {
|
|
80
|
+
if (!selectionSources[role] && source) {
|
|
81
|
+
selectionSources[role] = source;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function pushDistinctRef(refs, role, absolutePath, source) {
|
|
86
|
+
const existing = refs.find((item) => item.role === role && isSamePath(item.absolutePath, absolutePath));
|
|
87
|
+
if (!existing) {
|
|
88
|
+
refs.push({ role, absolutePath, source });
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function pushRoleConflict(issues, role, acceptedPath, incomingPath, acceptedSource, incomingSource) {
|
|
93
|
+
if (isSamePath(acceptedPath, incomingPath)) {
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
issues.blocking.push(createIssue(
|
|
98
|
+
"blocking",
|
|
99
|
+
`同时给出了多个${roleLabel(role)}:${String(acceptedPath).replaceAll("\\", "/")}(${sourceLabel(acceptedSource)}) 与 ${String(incomingPath).replaceAll("\\", "/")}(${sourceLabel(incomingSource)})。请只保留一个,或改用 ${role === "repo" ? "--repo" : "--docs"} 明确指定。`,
|
|
100
|
+
));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function assignDocsPath({
|
|
104
|
+
absolutePath,
|
|
105
|
+
source,
|
|
106
|
+
explicitRefs,
|
|
107
|
+
issues,
|
|
108
|
+
selectionSources,
|
|
109
|
+
currentDocsPath,
|
|
110
|
+
}) {
|
|
111
|
+
if (!currentDocsPath) {
|
|
112
|
+
pushDistinctRef(explicitRefs, "docs", absolutePath, source);
|
|
113
|
+
ensureSelectionSource(selectionSources, "docs", source);
|
|
114
|
+
return absolutePath;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
pushRoleConflict(issues, "docs", currentDocsPath, absolutePath, selectionSources.docs || source, source);
|
|
118
|
+
pushDistinctRef(explicitRefs, "docs", absolutePath, source);
|
|
119
|
+
return currentDocsPath;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function assignRepoRoot({
|
|
123
|
+
absolutePath,
|
|
124
|
+
source,
|
|
125
|
+
explicitRefs,
|
|
126
|
+
issues,
|
|
127
|
+
selectionSources,
|
|
128
|
+
currentRepoRoot,
|
|
129
|
+
allowNewRepoRoot = false,
|
|
130
|
+
}) {
|
|
131
|
+
if (!currentRepoRoot) {
|
|
132
|
+
pushDistinctRef(explicitRefs, allowNewRepoRoot ? "new_repo" : "repo", absolutePath, source);
|
|
133
|
+
ensureSelectionSource(selectionSources, "repo", source);
|
|
134
|
+
return absolutePath;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
pushRoleConflict(issues, "repo", currentRepoRoot, absolutePath, selectionSources.repo || source, source);
|
|
138
|
+
pushDistinctRef(explicitRefs, allowNewRepoRoot ? "new_repo" : "repo", absolutePath, source);
|
|
139
|
+
return currentRepoRoot;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function normalizeAnalyzeOptions(rawOptions = {}, cwd = process.cwd()) {
|
|
143
|
+
const options = {
|
|
144
|
+
...rawOptions,
|
|
145
|
+
requiredDocs: Array.isArray(rawOptions.requiredDocs) ? [...rawOptions.requiredDocs] : [],
|
|
146
|
+
constraints: Array.isArray(rawOptions.constraints) ? [...rawOptions.constraints] : [],
|
|
147
|
+
};
|
|
148
|
+
const positionals = Array.isArray(rawOptions.positionalArgs) ? rawOptions.positionalArgs : [];
|
|
149
|
+
const explicitRefs = [];
|
|
150
|
+
const requestTokens = [];
|
|
151
|
+
const issues = {
|
|
152
|
+
blocking: [],
|
|
153
|
+
warnings: [],
|
|
154
|
+
};
|
|
155
|
+
const selectionSources = {
|
|
156
|
+
...(rawOptions.selectionSources || {}),
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
let docsPath = options.docsPath ? resolveAbsolute(options.docsPath, cwd) : "";
|
|
160
|
+
let repoRoot = options.repoRoot ? resolveAbsolute(options.repoRoot, cwd) : "";
|
|
161
|
+
let inputPath = options.inputPath ? resolveAbsolute(options.inputPath, cwd) : "";
|
|
162
|
+
let allowNewRepoRoot = Boolean(options.allowNewRepoRoot);
|
|
163
|
+
|
|
164
|
+
if (docsPath) {
|
|
165
|
+
ensureSelectionSource(selectionSources, "docs", rawOptions.selectionSources?.docs || "flag");
|
|
166
|
+
}
|
|
167
|
+
if (repoRoot) {
|
|
168
|
+
ensureSelectionSource(selectionSources, "repo", rawOptions.selectionSources?.repo || "flag");
|
|
169
|
+
if (allowNewRepoRoot && !pathExists(repoRoot)) {
|
|
170
|
+
pushDistinctRef(explicitRefs, "new_repo", repoRoot, selectionSources.repo);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
for (const token of positionals) {
|
|
175
|
+
if (!isProbablyPathToken(token)) {
|
|
176
|
+
requestTokens.push(token);
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const absolutePath = resolveAbsolute(token, cwd);
|
|
181
|
+
if (pathExists(absolutePath)) {
|
|
182
|
+
const classified = classifyExplicitPath(absolutePath);
|
|
183
|
+
if (classified.kind === "docs") {
|
|
184
|
+
docsPath = assignDocsPath({
|
|
185
|
+
absolutePath: classified.absolutePath,
|
|
186
|
+
source: "positional",
|
|
187
|
+
explicitRefs,
|
|
188
|
+
issues,
|
|
189
|
+
selectionSources,
|
|
190
|
+
currentDocsPath: docsPath,
|
|
191
|
+
});
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
if (classified.kind === "repo") {
|
|
195
|
+
repoRoot = assignRepoRoot({
|
|
196
|
+
absolutePath: classified.absolutePath,
|
|
197
|
+
source: "positional",
|
|
198
|
+
explicitRefs,
|
|
199
|
+
issues,
|
|
200
|
+
selectionSources,
|
|
201
|
+
currentRepoRoot: repoRoot,
|
|
202
|
+
});
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
if ((classified.kind === "workspace" || classified.kind === "directory") && docsPath) {
|
|
206
|
+
repoRoot = assignRepoRoot({
|
|
207
|
+
absolutePath: classified.absolutePath,
|
|
208
|
+
source: "positional",
|
|
209
|
+
explicitRefs,
|
|
210
|
+
issues,
|
|
211
|
+
selectionSources,
|
|
212
|
+
currentRepoRoot: repoRoot,
|
|
213
|
+
});
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
if (!inputPath) {
|
|
217
|
+
inputPath = classified.absolutePath;
|
|
218
|
+
pushDistinctRef(explicitRefs, "input", classified.absolutePath, "positional");
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
} else if (!repoRoot) {
|
|
222
|
+
repoRoot = absolutePath;
|
|
223
|
+
allowNewRepoRoot = true;
|
|
224
|
+
ensureSelectionSource(selectionSources, "repo", "positional");
|
|
225
|
+
pushDistinctRef(explicitRefs, "new_repo", absolutePath, "positional");
|
|
226
|
+
continue;
|
|
227
|
+
} else {
|
|
228
|
+
pushRoleConflict(issues, "repo", repoRoot, absolutePath, selectionSources.repo || "positional", "positional");
|
|
229
|
+
pushDistinctRef(explicitRefs, "new_repo", absolutePath, "positional");
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
requestTokens.push(token);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (options.dryRun && options.yes) {
|
|
237
|
+
issues.warnings.push(createIssue(
|
|
238
|
+
"warning",
|
|
239
|
+
"同时传入了 --dry-run 和 -y;本次仍按 --dry-run 只分析,不自动执行。",
|
|
240
|
+
));
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const userRequestText = requestTokens.join(" ").trim();
|
|
244
|
+
|
|
245
|
+
options.docsPath = docsPath;
|
|
246
|
+
options.repoRoot = repoRoot;
|
|
247
|
+
options.inputPath = inputPath;
|
|
248
|
+
options.allowNewRepoRoot = allowNewRepoRoot;
|
|
249
|
+
options.selectionSources = selectionSources;
|
|
250
|
+
options.userRequestText = userRequestText;
|
|
251
|
+
options.inputIssues = issues;
|
|
252
|
+
options.userIntent = {
|
|
253
|
+
rawPositionals: positionals,
|
|
254
|
+
explicitRefs,
|
|
255
|
+
requestText: userRequestText,
|
|
256
|
+
issues,
|
|
257
|
+
selectionSources,
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
return options;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
export function hasBlockingInputIssues(inputIssues = {}) {
|
|
264
|
+
return Array.isArray(inputIssues.blocking) && inputIssues.blocking.length > 0;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
export function renderInputIssueLines(inputIssues = {}) {
|
|
268
|
+
return [
|
|
269
|
+
...(Array.isArray(inputIssues.blocking) ? inputIssues.blocking : []),
|
|
270
|
+
...(Array.isArray(inputIssues.warnings) ? inputIssues.warnings : []),
|
|
271
|
+
].map((item) => `- ${item.message}`);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
export function renderBlockingInputIssueMessage(inputIssues = {}) {
|
|
275
|
+
const lines = renderInputIssueLines({
|
|
276
|
+
blocking: Array.isArray(inputIssues.blocking) ? inputIssues.blocking : [],
|
|
277
|
+
});
|
|
278
|
+
if (!lines.length) {
|
|
279
|
+
return "";
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
return [
|
|
283
|
+
"检测到命令输入存在冲突:",
|
|
284
|
+
...lines,
|
|
285
|
+
"",
|
|
286
|
+
"请整理后重试;建议只保留一个开发文档路径和一个项目路径,必要时改用 `--docs` / `--repo` 明确指定。",
|
|
287
|
+
].join("\n");
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
export function renderUserIntentLines(userIntent = {}) {
|
|
291
|
+
const lines = [];
|
|
292
|
+
const explicitRefs = Array.isArray(userIntent.explicitRefs) ? userIntent.explicitRefs : [];
|
|
293
|
+
const requestText = String(userIntent.requestText || "").trim();
|
|
294
|
+
|
|
295
|
+
if (explicitRefs.length) {
|
|
296
|
+
lines.push(...explicitRefs.map((ref) => formatPathRef(ref)));
|
|
297
|
+
}
|
|
298
|
+
if (requestText) {
|
|
299
|
+
lines.push(`附加要求:${requestText}`);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
return lines;
|
|
303
|
+
}
|
package/src/analyzer.mjs
CHANGED
|
@@ -55,6 +55,29 @@ function normalizeAnalysisPayload(payload, docsEntries) {
|
|
|
55
55
|
throw new Error("Codex 分析结果无效:未生成可用任务。");
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
+
const requestInterpretation = payload?.requestInterpretation && typeof payload.requestInterpretation === "object"
|
|
59
|
+
? {
|
|
60
|
+
summary: String(payload.requestInterpretation.summary || "").trim(),
|
|
61
|
+
priorities: Array.isArray(payload.requestInterpretation.priorities)
|
|
62
|
+
? payload.requestInterpretation.priorities.map((item) => String(item || "").trim()).filter(Boolean)
|
|
63
|
+
: [],
|
|
64
|
+
cautions: Array.isArray(payload.requestInterpretation.cautions)
|
|
65
|
+
? payload.requestInterpretation.cautions.map((item) => String(item || "").trim()).filter(Boolean)
|
|
66
|
+
: [],
|
|
67
|
+
}
|
|
68
|
+
: null;
|
|
69
|
+
const repoDecision = payload?.repoDecision && typeof payload.repoDecision === "object"
|
|
70
|
+
? {
|
|
71
|
+
compatibility: ["compatible", "conflict", "uncertain"].includes(String(payload.repoDecision.compatibility || ""))
|
|
72
|
+
? String(payload.repoDecision.compatibility)
|
|
73
|
+
: "compatible",
|
|
74
|
+
action: ["continue_existing", "confirm_rebuild", "start_new"].includes(String(payload.repoDecision.action || ""))
|
|
75
|
+
? String(payload.repoDecision.action)
|
|
76
|
+
: "continue_existing",
|
|
77
|
+
reason: String(payload.repoDecision.reason || "").trim(),
|
|
78
|
+
}
|
|
79
|
+
: null;
|
|
80
|
+
|
|
58
81
|
return {
|
|
59
82
|
project: String(payload.project || "").trim() || "helloloop-project",
|
|
60
83
|
summary: {
|
|
@@ -66,6 +89,16 @@ function normalizeAnalysisPayload(payload, docsEntries) {
|
|
|
66
89
|
constraints: Array.isArray(payload.constraints)
|
|
67
90
|
? payload.constraints.map((item) => String(item || "").trim()).filter(Boolean)
|
|
68
91
|
: [],
|
|
92
|
+
requestInterpretation: requestInterpretation && (
|
|
93
|
+
requestInterpretation.summary
|
|
94
|
+
|| requestInterpretation.priorities.length
|
|
95
|
+
|| requestInterpretation.cautions.length
|
|
96
|
+
)
|
|
97
|
+
? requestInterpretation
|
|
98
|
+
: null,
|
|
99
|
+
repoDecision: repoDecision && repoDecision.reason
|
|
100
|
+
? repoDecision
|
|
101
|
+
: null,
|
|
69
102
|
tasks,
|
|
70
103
|
requiredDocs: docsEntries,
|
|
71
104
|
};
|
|
@@ -99,6 +132,7 @@ export async function analyzeWorkspace(options = {}) {
|
|
|
99
132
|
repoRoot: options.repoRoot,
|
|
100
133
|
docsPath: options.docsPath,
|
|
101
134
|
configDirName: options.configDirName,
|
|
135
|
+
allowNewRepoRoot: options.allowNewRepoRoot,
|
|
102
136
|
});
|
|
103
137
|
|
|
104
138
|
if (!discovery.ok) {
|
|
@@ -106,6 +140,7 @@ export async function analyzeWorkspace(options = {}) {
|
|
|
106
140
|
ok: false,
|
|
107
141
|
code: discovery.code,
|
|
108
142
|
summary: discovery.message,
|
|
143
|
+
discovery,
|
|
109
144
|
};
|
|
110
145
|
}
|
|
111
146
|
|
|
@@ -125,11 +160,13 @@ export async function analyzeWorkspace(options = {}) {
|
|
|
125
160
|
|
|
126
161
|
const prompt = buildAnalysisPrompt({
|
|
127
162
|
repoRoot: context.repoRoot,
|
|
163
|
+
repoOriginallyExisted: discovery?.resolution?.repo?.exists !== false,
|
|
128
164
|
docsEntries: discovery.docsEntries,
|
|
129
165
|
docPackets,
|
|
130
166
|
existingStateText,
|
|
131
167
|
existingBacklogText,
|
|
132
168
|
existingProjectConstraints: existingProjectConfig.constraints,
|
|
169
|
+
userIntent: options.userIntent,
|
|
133
170
|
});
|
|
134
171
|
|
|
135
172
|
const runDir = path.join(context.runsDir, `${nowIso().replaceAll(":", "-").replaceAll(".", "-")}-analysis`);
|
|
@@ -204,5 +241,6 @@ export async function analyzeWorkspace(options = {}) {
|
|
|
204
241
|
analysis,
|
|
205
242
|
backlog,
|
|
206
243
|
summary: buildAnalysisSummaryText(context, analysis, backlog),
|
|
244
|
+
discovery,
|
|
207
245
|
};
|
|
208
246
|
}
|