@clue-ai/cli 0.0.9 → 0.0.11
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 +37 -26
- package/bin/clue-cli.mjs +93 -54
- package/package.json +1 -1
- package/src/cli-invocation.mjs +34 -0
- package/src/command-spec.mjs +3 -0
- package/src/init-tool.mjs +2 -1
- package/src/lifecycle-guard.mjs +168 -103
- package/src/lifecycle-init.mjs +593 -187
- package/src/semantic-agent-runner.mjs +3 -1
- package/src/setup-check.mjs +641 -47
- package/src/setup-help.mjs +69 -0
- package/src/setup-prepare.mjs +78 -15
- package/src/setup-tool.mjs +421 -388
package/src/lifecycle-guard.mjs
CHANGED
|
@@ -1,134 +1,197 @@
|
|
|
1
1
|
const LIFECYCLE_CALL_PATTERN =
|
|
2
|
-
/\b(ClueInit|ClueIdentify|ClueSetAccount|ClueLogout)\s*\(/g;
|
|
3
|
-
const SAFE_HELPER_PATTERN =
|
|
4
|
-
/\b(?:safeClue|safe_clue|safe_clue_call|safeClueCall|withClueGuard|with_clue_guard)\s*\(/g;
|
|
2
|
+
/\b(ClueInit|ClueIdentify|ClueSetAccount|ClueLogout|clue_init_fastapi|clue_init_django)\s*\(/g;
|
|
5
3
|
|
|
6
|
-
const
|
|
7
|
-
let
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
4
|
+
export const stripSourceNoise = (text, { stripStrings = false } = {}) => {
|
|
5
|
+
let output = "";
|
|
6
|
+
let index = 0;
|
|
7
|
+
while (index < text.length) {
|
|
8
|
+
const char = text[index];
|
|
9
|
+
const next = text[index + 1];
|
|
10
|
+
if (char === "/" && next === "/") {
|
|
11
|
+
while (index < text.length && text[index] !== "\n") index += 1;
|
|
12
|
+
continue;
|
|
13
|
+
}
|
|
14
|
+
if (char === "#") {
|
|
15
|
+
while (index < text.length && text[index] !== "\n") index += 1;
|
|
16
|
+
continue;
|
|
14
17
|
}
|
|
18
|
+
if (char === "/" && next === "*") {
|
|
19
|
+
index += 2;
|
|
20
|
+
while (
|
|
21
|
+
index < text.length &&
|
|
22
|
+
!(text[index] === "*" && text[index + 1] === "/")
|
|
23
|
+
) {
|
|
24
|
+
if (text[index] === "\n") output += "\n";
|
|
25
|
+
index += 1;
|
|
26
|
+
}
|
|
27
|
+
index += index < text.length ? 2 : 0;
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
if (char === "'" || char === '"' || char === "`") {
|
|
31
|
+
const quote = char;
|
|
32
|
+
const triple =
|
|
33
|
+
quote !== "`" && text.slice(index, index + 3) === quote.repeat(3);
|
|
34
|
+
const endToken = triple ? quote.repeat(3) : quote;
|
|
35
|
+
if (!stripStrings) {
|
|
36
|
+
output += triple ? endToken : quote;
|
|
37
|
+
} else {
|
|
38
|
+
output += quote === "`" ? "``" : `${quote}${quote}`;
|
|
39
|
+
}
|
|
40
|
+
index += triple ? 3 : 1;
|
|
41
|
+
while (index < text.length) {
|
|
42
|
+
if (!triple && text[index] === "\\") {
|
|
43
|
+
if (!stripStrings) output += text.slice(index, index + 2);
|
|
44
|
+
index += 2;
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
if (text.slice(index, index + endToken.length) === endToken) {
|
|
48
|
+
if (!stripStrings) output += endToken;
|
|
49
|
+
index += endToken.length;
|
|
50
|
+
break;
|
|
51
|
+
}
|
|
52
|
+
if (text[index] === "\n") output += "\n";
|
|
53
|
+
if (!stripStrings && text[index] !== "\n") output += text[index];
|
|
54
|
+
index += 1;
|
|
55
|
+
}
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
output += char;
|
|
59
|
+
index += 1;
|
|
15
60
|
}
|
|
16
|
-
return
|
|
61
|
+
return output;
|
|
17
62
|
};
|
|
18
63
|
|
|
19
|
-
const
|
|
20
|
-
|
|
64
|
+
const isIdentifierCharacter = (character) =>
|
|
65
|
+
typeof character === "string" && /[A-Za-z0-9_$]/.test(character);
|
|
21
66
|
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
67
|
+
const startsKeyword = (text, index, keyword) =>
|
|
68
|
+
text.startsWith(keyword, index) &&
|
|
69
|
+
!isIdentifierCharacter(text[index - 1]) &&
|
|
70
|
+
!isIdentifierCharacter(text[index + keyword.length]);
|
|
26
71
|
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
72
|
+
const advanceQuotedString = (text, startIndex, { preserve = false } = {}) => {
|
|
73
|
+
const quote = text[startIndex];
|
|
74
|
+
const triple = quote !== "`" && text.slice(startIndex, startIndex + 3) === quote.repeat(3);
|
|
75
|
+
const endToken = triple ? quote.repeat(3) : quote;
|
|
76
|
+
let index = startIndex + (triple ? 3 : 1);
|
|
77
|
+
let value = preserve ? (triple ? endToken : quote) : "";
|
|
78
|
+
while (index < text.length) {
|
|
79
|
+
if (!triple && text[index] === "\\") {
|
|
80
|
+
if (preserve) value += text.slice(index, index + 2);
|
|
81
|
+
index += 2;
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
if (text.slice(index, index + endToken.length) === endToken) {
|
|
85
|
+
if (preserve) value += endToken;
|
|
86
|
+
index += endToken.length;
|
|
87
|
+
break;
|
|
88
|
+
}
|
|
89
|
+
if (preserve) value += text[index];
|
|
90
|
+
index += 1;
|
|
39
91
|
}
|
|
40
|
-
return
|
|
92
|
+
return { index, value };
|
|
41
93
|
};
|
|
42
94
|
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
const
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
)
|
|
54
|
-
|
|
95
|
+
export const extractExecutableModuleStatements = (text) => {
|
|
96
|
+
const statements = [];
|
|
97
|
+
let index = 0;
|
|
98
|
+
while (index < text.length) {
|
|
99
|
+
const char = text[index];
|
|
100
|
+
const next = text[index + 1];
|
|
101
|
+
if (char === "/" && next === "/") {
|
|
102
|
+
while (index < text.length && text[index] !== "\n") index += 1;
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
if (char === "#") {
|
|
106
|
+
while (index < text.length && text[index] !== "\n") index += 1;
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
if (char === "/" && next === "*") {
|
|
110
|
+
index += 2;
|
|
111
|
+
while (
|
|
112
|
+
index < text.length &&
|
|
113
|
+
!(text[index] === "*" && text[index + 1] === "/")
|
|
114
|
+
) {
|
|
115
|
+
index += 1;
|
|
116
|
+
}
|
|
117
|
+
index += index < text.length ? 2 : 0;
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
if (char === "'" || char === '"' || char === "`") {
|
|
121
|
+
index = advanceQuotedString(text, index).index;
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
if (!startsKeyword(text, index, "import") && !startsKeyword(text, index, "export")) {
|
|
125
|
+
index += 1;
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
let statement = "";
|
|
129
|
+
let depth = 0;
|
|
130
|
+
while (index < text.length) {
|
|
131
|
+
const current = text[index];
|
|
132
|
+
const following = text[index + 1];
|
|
133
|
+
if (current === "/" && following === "/") {
|
|
134
|
+
while (index < text.length && text[index] !== "\n") index += 1;
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
if (current === "/" && following === "*") {
|
|
138
|
+
index += 2;
|
|
139
|
+
while (
|
|
140
|
+
index < text.length &&
|
|
141
|
+
!(text[index] === "*" && text[index + 1] === "/")
|
|
142
|
+
) {
|
|
143
|
+
index += 1;
|
|
144
|
+
}
|
|
145
|
+
index += index < text.length ? 2 : 0;
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
if (current === "'" || current === '"' || current === "`") {
|
|
149
|
+
const quoted = advanceQuotedString(text, index, { preserve: true });
|
|
150
|
+
statement += quoted.value;
|
|
151
|
+
index = quoted.index;
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
statement += current;
|
|
155
|
+
if (current === "{" || current === "(" || current === "[") depth += 1;
|
|
156
|
+
if (current === "}" || current === ")" || current === "]") {
|
|
157
|
+
depth = Math.max(0, depth - 1);
|
|
158
|
+
}
|
|
159
|
+
index += 1;
|
|
160
|
+
if (current === ";" && depth === 0) break;
|
|
161
|
+
if (current === "\n" && depth === 0) break;
|
|
162
|
+
}
|
|
163
|
+
const trimmed = statement.trim();
|
|
164
|
+
if (trimmed) statements.push(trimmed);
|
|
55
165
|
}
|
|
56
|
-
return
|
|
166
|
+
return statements;
|
|
57
167
|
};
|
|
58
168
|
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
let start = 0;
|
|
62
|
-
for (const line of text.split("\n")) {
|
|
63
|
-
const indent = line.match(/^\s*/)?.[0].length ?? 0;
|
|
64
|
-
lines.push({
|
|
65
|
-
start,
|
|
66
|
-
end: start + line.length,
|
|
67
|
-
indent,
|
|
68
|
-
text: line,
|
|
69
|
-
});
|
|
70
|
-
start += line.length + 1;
|
|
71
|
-
}
|
|
72
|
-
return lines;
|
|
73
|
-
};
|
|
169
|
+
const lineNumberForIndex = (text, index) =>
|
|
170
|
+
text.slice(0, index).split("\n").length;
|
|
74
171
|
|
|
75
|
-
const
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
(line) => line.start <= callIndex && callIndex <= line.end,
|
|
79
|
-
);
|
|
80
|
-
if (callLineIndex < 0) return false;
|
|
81
|
-
const callLine = lines[callLineIndex];
|
|
82
|
-
for (let index = callLineIndex - 1; index >= 0; index -= 1) {
|
|
83
|
-
const candidate = lines[index];
|
|
84
|
-
if (!candidate.text.trim()) continue;
|
|
85
|
-
if (!/^\s*try\s*:\s*(?:#.*)?$/.test(candidate.text)) continue;
|
|
86
|
-
if (candidate.indent >= callLine.indent) continue;
|
|
87
|
-
const escaped = lines
|
|
88
|
-
.slice(index + 1, callLineIndex)
|
|
89
|
-
.some(
|
|
90
|
-
(line) =>
|
|
91
|
-
line.text.trim() &&
|
|
92
|
-
line.indent <= candidate.indent &&
|
|
93
|
-
!/^\s*(?:except|finally|else)\b/.test(line.text),
|
|
94
|
-
);
|
|
95
|
-
if (!escaped) return true;
|
|
96
|
-
}
|
|
97
|
-
return false;
|
|
172
|
+
const isAwaitedOnCallLine = (text, callIndex) => {
|
|
173
|
+
const lineStart = text.lastIndexOf("\n", callIndex - 1) + 1;
|
|
174
|
+
return /\bawait\b/.test(text.slice(lineStart, callIndex));
|
|
98
175
|
};
|
|
99
176
|
|
|
100
|
-
const
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
return
|
|
177
|
+
const canonicalLifecycleApiName = (apiName) => {
|
|
178
|
+
if (apiName === "clue_init_fastapi" || apiName === "clue_init_django") {
|
|
179
|
+
return "ClueInit";
|
|
180
|
+
}
|
|
181
|
+
return apiName;
|
|
105
182
|
};
|
|
106
183
|
|
|
107
|
-
const isGuardedLifecycleCall = (text, callIndex) =>
|
|
108
|
-
hasCatchHandlerOnCall(text, callIndex) ||
|
|
109
|
-
isInsideSafeHelperCall(text, callIndex) ||
|
|
110
|
-
isInsideJsTryBlock(text, callIndex) ||
|
|
111
|
-
isInsidePythonTryBlock(text, callIndex);
|
|
112
|
-
|
|
113
184
|
export const findLifecycleGuardViolations = (text) => {
|
|
114
185
|
const violations = [];
|
|
115
186
|
for (const match of text.matchAll(LIFECYCLE_CALL_PATTERN)) {
|
|
116
187
|
const callIndex = match.index ?? 0;
|
|
117
|
-
const apiName = match[1];
|
|
188
|
+
const apiName = canonicalLifecycleApiName(match[1]);
|
|
118
189
|
if (isAwaitedOnCallLine(text, callIndex)) {
|
|
119
190
|
violations.push({
|
|
120
191
|
api_name: apiName,
|
|
121
192
|
line: lineNumberForIndex(text, callIndex),
|
|
122
193
|
reason: "awaited_lifecycle_call",
|
|
123
194
|
});
|
|
124
|
-
continue;
|
|
125
|
-
}
|
|
126
|
-
if (!isGuardedLifecycleCall(text, callIndex)) {
|
|
127
|
-
violations.push({
|
|
128
|
-
api_name: apiName,
|
|
129
|
-
line: lineNumberForIndex(text, callIndex),
|
|
130
|
-
reason: "unguarded_lifecycle_call",
|
|
131
|
-
});
|
|
132
195
|
}
|
|
133
196
|
}
|
|
134
197
|
return violations;
|
|
@@ -136,6 +199,8 @@ export const findLifecycleGuardViolations = (text) => {
|
|
|
136
199
|
|
|
137
200
|
export const findLifecycleCallApiNames = (text) => [
|
|
138
201
|
...new Set(
|
|
139
|
-
[...text.matchAll(LIFECYCLE_CALL_PATTERN)].map((match) =>
|
|
202
|
+
[...text.matchAll(LIFECYCLE_CALL_PATTERN)].map((match) =>
|
|
203
|
+
canonicalLifecycleApiName(match[1]),
|
|
204
|
+
),
|
|
140
205
|
),
|
|
141
206
|
];
|