@abelxiaoxing/cadence 1.0.0

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.
@@ -0,0 +1,414 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { writeFileSync } from "node:fs";
4
+ import { homedir } from "node:os";
5
+ import path from "node:path";
6
+ import { pathToFileURL } from "node:url";
7
+ import { requestJson } from "../_shared/http-client.mjs";
8
+ import { loadConfig } from "../_shared/load-config.mjs";
9
+
10
+ const DEFAULT_MODEL = "grok-4.20-non-reasoning";
11
+ const DEFAULT_TAVILY_URL = "https://api.tavily.com";
12
+ const TAVILY_MAX_RESULTS = 20;
13
+
14
+ const trimBase = (value) => value.replace(/\/+$/, "");
15
+
16
+ export function loadGrokConfig({ cwd, home }) {
17
+ const selected = loadConfig({
18
+ cwd,
19
+ home,
20
+ required: ["GROK_API_URL", "GROK_API_KEY"],
21
+ });
22
+ const tavilyKey = selected.values.TAVILY_API_KEY ?? "";
23
+ return {
24
+ path: selected.path,
25
+ grokUrl: selected.values.GROK_API_URL,
26
+ grokKey: selected.values.GROK_API_KEY,
27
+ model: selected.values.GROK_MODEL || DEFAULT_MODEL,
28
+ tavilyUrl: selected.values.TAVILY_API_URL || DEFAULT_TAVILY_URL,
29
+ tavilyKey,
30
+ tavilyEnabled: selected.values.TAVILY_ENABLED !== "false",
31
+ tavilyConfigured: Boolean(tavilyKey),
32
+ };
33
+ }
34
+
35
+ function parseOptions(args, allowed) {
36
+ const options = {};
37
+ for (let index = 0; index < args.length; index += 2) {
38
+ const option = args[index];
39
+ const value = args[index + 1];
40
+ if (
41
+ !option?.startsWith("--") ||
42
+ !allowed.has(option) ||
43
+ value === undefined
44
+ ) {
45
+ throw new Error(`Invalid option near ${option ?? "end of command"}`);
46
+ }
47
+ if (Object.hasOwn(options, option))
48
+ throw new Error(`Duplicate option ${option}`);
49
+ options[option] = value;
50
+ }
51
+ return options;
52
+ }
53
+
54
+ function integerOption(
55
+ name,
56
+ value,
57
+ { min = 0, max = Number.MAX_SAFE_INTEGER } = {},
58
+ ) {
59
+ if (!/^(0|[1-9]\d*)$/.test(value)) {
60
+ throw new Error(`${name} must be a canonical non-negative integer`);
61
+ }
62
+ const number = Number(value);
63
+ if (!Number.isSafeInteger(number))
64
+ throw new Error(`${name} must be a safe integer`);
65
+ if (number < min || number > max) {
66
+ throw new Error(
67
+ `${name} integer is outside the supported range ${min}-${max}`,
68
+ );
69
+ }
70
+ return number;
71
+ }
72
+
73
+ function httpUrl(value, name) {
74
+ let parsed;
75
+ try {
76
+ parsed = new URL(value);
77
+ } catch {
78
+ throw new Error(`${name} must be an HTTP or HTTPS URL`);
79
+ }
80
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
81
+ throw new Error(`${name} must be an HTTP or HTTPS URL`);
82
+ }
83
+ return value.trim();
84
+ }
85
+
86
+ function grokHeaders(key) {
87
+ return {
88
+ Accept: "application/json",
89
+ Authorization: `Bearer ${key}`,
90
+ "Content-Type": "application/json",
91
+ };
92
+ }
93
+
94
+ function tavilyHeaders(key) {
95
+ return {
96
+ Accept: "application/json",
97
+ Authorization: `Bearer ${key}`,
98
+ "Content-Type": "application/json",
99
+ };
100
+ }
101
+
102
+ function requireTavily(config) {
103
+ if (!config.tavilyEnabled || !config.tavilyConfigured) {
104
+ throw new Error(
105
+ "Tavily is required and must be enabled with a non-empty API key",
106
+ );
107
+ }
108
+ }
109
+
110
+ async function grokChat({ config, model, system, user, fetchImpl, sleep }) {
111
+ const response = await requestJson({
112
+ operation: "Grok Chat Completions",
113
+ url: `${trimBase(config.grokUrl)}/chat/completions`,
114
+ method: "POST",
115
+ headers: grokHeaders(config.grokKey),
116
+ body: {
117
+ model: model || config.model,
118
+ stream: false,
119
+ messages: [
120
+ { role: "system", content: system },
121
+ { role: "user", content: user },
122
+ ],
123
+ },
124
+ fetchImpl,
125
+ sleep,
126
+ });
127
+ const content = response?.choices?.[0]?.message?.content;
128
+ if (typeof content !== "string" || !content.trim()) {
129
+ throw new Error("Invalid Grok Chat Completions response shape");
130
+ }
131
+ return content;
132
+ }
133
+
134
+ function normalizeResult(item) {
135
+ if (!item || typeof item !== "object")
136
+ throw new Error("Invalid search result contract");
137
+ const title = typeof item.title === "string" ? item.title : "";
138
+ const description =
139
+ typeof item.description === "string"
140
+ ? item.description
141
+ : typeof item.content === "string"
142
+ ? item.content
143
+ : "";
144
+ const url = httpUrl(item.url, "Search result URL");
145
+ if (!title || !description) throw new Error("Invalid search result contract");
146
+ return { title, url, description };
147
+ }
148
+
149
+ function normalizeResults(value) {
150
+ if (!Array.isArray(value)) throw new Error("Invalid search result contract");
151
+ return value.map(normalizeResult);
152
+ }
153
+
154
+ export function mergeSources(grokResults, tavilyResults) {
155
+ const merged = [];
156
+ const seen = new Set();
157
+ for (const item of [...grokResults, ...tavilyResults]) {
158
+ if (seen.has(item.url)) continue;
159
+ seen.add(item.url);
160
+ merged.push(item);
161
+ }
162
+ return merged;
163
+ }
164
+
165
+ async function tavilyRequest({ config, endpoint, body, fetchImpl, sleep }) {
166
+ return requestJson({
167
+ operation: `Tavily ${endpoint}`,
168
+ url: `${trimBase(config.tavilyUrl)}/${endpoint}`,
169
+ method: "POST",
170
+ headers: tavilyHeaders(config.tavilyKey),
171
+ body,
172
+ fetchImpl,
173
+ sleep,
174
+ });
175
+ }
176
+
177
+ function parseSearchContent(content) {
178
+ try {
179
+ return normalizeResults(JSON.parse(content));
180
+ } catch (error) {
181
+ if (error instanceof SyntaxError)
182
+ throw new Error("Grok search returned malformed JSON");
183
+ throw error;
184
+ }
185
+ }
186
+
187
+ async function webSearch({ options, config, fetchImpl, sleep }) {
188
+ const query = options["--query"]?.trim();
189
+ if (!query) throw new Error("web_search requires --query");
190
+ const minResults = integerOption(
191
+ "--min-results",
192
+ options["--min-results"] ?? "1",
193
+ {
194
+ min: 1,
195
+ },
196
+ );
197
+ const maxResults = integerOption(
198
+ "--max-results",
199
+ options["--max-results"] ?? "10",
200
+ {
201
+ min: 1,
202
+ },
203
+ );
204
+ if (maxResults < minResults)
205
+ throw new Error("--max-results must be at least --min-results");
206
+ const extraSources = integerOption(
207
+ "--extra-sources",
208
+ options["--extra-sources"] ?? "0",
209
+ {
210
+ max: TAVILY_MAX_RESULTS,
211
+ },
212
+ );
213
+
214
+ let tavilyResults = [];
215
+ if (extraSources > 0) {
216
+ requireTavily(config);
217
+ const response = await tavilyRequest({
218
+ config,
219
+ endpoint: "search",
220
+ body: { query, max_results: extraSources },
221
+ fetchImpl,
222
+ sleep,
223
+ });
224
+ tavilyResults = normalizeResults(response?.results);
225
+ }
226
+
227
+ const platform = options["--platform"]?.trim();
228
+ const user = platform ? `${query}\nPlatform constraint: ${platform}` : query;
229
+ const content = await grokChat({
230
+ config,
231
+ model: options["--model"],
232
+ system: `Return only a JSON array of ${minResults}-${maxResults} objects with string title, HTTP(S) url, and description fields. Do not stream or add prose.`,
233
+ user,
234
+ fetchImpl,
235
+ sleep,
236
+ });
237
+ const grokResults = parseSearchContent(content);
238
+ if (grokResults.length < minResults || grokResults.length > maxResults) {
239
+ throw new Error(
240
+ "Grok search result contract did not satisfy requested result counts",
241
+ );
242
+ }
243
+ return mergeSources(grokResults, tavilyResults);
244
+ }
245
+
246
+ async function webFetch({ options, config, fetchImpl, sleep, warn }) {
247
+ const requestedUrl = httpUrl(options["--url"], "--url");
248
+ let markdown;
249
+ if (config.tavilyEnabled && config.tavilyConfigured) {
250
+ try {
251
+ const response = await tavilyRequest({
252
+ config,
253
+ endpoint: "extract",
254
+ body: { urls: [requestedUrl], format: "markdown" },
255
+ fetchImpl,
256
+ sleep,
257
+ });
258
+ markdown = response?.results?.[0]?.raw_content;
259
+ if (typeof markdown !== "string" || !markdown) {
260
+ throw new Error("Invalid Tavily extract response shape");
261
+ }
262
+ } catch (error) {
263
+ warn(
264
+ `Tavily extract warning: ${error instanceof Error ? error.message : "request failed"}`,
265
+ );
266
+ }
267
+ }
268
+
269
+ if (!markdown) {
270
+ markdown = await grokChat({
271
+ config,
272
+ system:
273
+ "Retrieve the requested URL and return structured Markdown only. Do not stream.",
274
+ user: requestedUrl,
275
+ fetchImpl,
276
+ sleep,
277
+ });
278
+ }
279
+
280
+ const output = options["--out"];
281
+ if (output) {
282
+ writeFileSync(output, markdown);
283
+ return { out: output };
284
+ }
285
+ return markdown;
286
+ }
287
+
288
+ async function webMap({ options, config, fetchImpl, sleep }) {
289
+ requireTavily(config);
290
+ const requestedUrl = httpUrl(options["--url"], "--url");
291
+ const maxDepth = integerOption("--max-depth", options["--max-depth"] ?? "2", {
292
+ min: 1,
293
+ max: 5,
294
+ });
295
+ const maxBreadth = integerOption(
296
+ "--max-breadth",
297
+ options["--max-breadth"] ?? "20",
298
+ {
299
+ min: 1,
300
+ },
301
+ );
302
+ const limit = integerOption("--limit", options["--limit"] ?? "50", {
303
+ min: 1,
304
+ });
305
+ const response = await tavilyRequest({
306
+ config,
307
+ endpoint: "map",
308
+ body: {
309
+ url: requestedUrl,
310
+ ...(options["--instructions"]
311
+ ? { instructions: options["--instructions"] }
312
+ : {}),
313
+ max_depth: maxDepth,
314
+ max_breadth: maxBreadth,
315
+ limit,
316
+ },
317
+ fetchImpl,
318
+ sleep,
319
+ });
320
+ if (!response || !Array.isArray(response.results)) {
321
+ throw new Error("Invalid Tavily map response shape");
322
+ }
323
+ return response;
324
+ }
325
+
326
+ export async function runGrokSearch({
327
+ argv,
328
+ cwd,
329
+ home,
330
+ fetchImpl = fetch,
331
+ sleep,
332
+ warn = (message) => console.error(message),
333
+ }) {
334
+ const [command, ...args] = argv;
335
+ const config = loadGrokConfig({ cwd, home });
336
+
337
+ if (command === "get_config_info") {
338
+ if (args.length > 0) throw new Error("get_config_info accepts no options");
339
+ return {
340
+ configPath: config.path,
341
+ grokApiUrl: trimBase(config.grokUrl),
342
+ model: config.model,
343
+ grokConfigured: Boolean(config.grokKey),
344
+ tavilyApiUrl: trimBase(config.tavilyUrl),
345
+ tavilyEnabled: config.tavilyEnabled,
346
+ tavilyConfigured: config.tavilyConfigured,
347
+ };
348
+ }
349
+ if (command === "web_search") {
350
+ const options = parseOptions(
351
+ args,
352
+ new Set([
353
+ "--query",
354
+ "--platform",
355
+ "--min-results",
356
+ "--max-results",
357
+ "--model",
358
+ "--extra-sources",
359
+ ]),
360
+ );
361
+ return webSearch({ options, config, fetchImpl, sleep });
362
+ }
363
+ if (command === "web_fetch") {
364
+ const options = parseOptions(args, new Set(["--url", "--out"]));
365
+ return webFetch({ options, config, fetchImpl, sleep, warn });
366
+ }
367
+ if (command === "web_map") {
368
+ const options = parseOptions(
369
+ args,
370
+ new Set([
371
+ "--url",
372
+ "--instructions",
373
+ "--max-depth",
374
+ "--max-breadth",
375
+ "--limit",
376
+ ]),
377
+ );
378
+ return webMap({ options, config, fetchImpl, sleep });
379
+ }
380
+ throw new Error(
381
+ "Unknown command; expected web_search, web_fetch, web_map, or get_config_info",
382
+ );
383
+ }
384
+
385
+ function printUsage() {
386
+ process.stderr.write(
387
+ "Usage: node grok-search.mjs web_search|web_fetch|web_map|get_config_info [options]\n",
388
+ );
389
+ }
390
+
391
+ const isMain =
392
+ process.argv[1] &&
393
+ import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href;
394
+
395
+ if (isMain) {
396
+ try {
397
+ const result = await runGrokSearch({
398
+ argv: process.argv.slice(2),
399
+ cwd: process.cwd(),
400
+ home: homedir(),
401
+ });
402
+ process.stdout.write(
403
+ typeof result === "string"
404
+ ? `${result}\n`
405
+ : `${JSON.stringify(result, null, 2)}\n`,
406
+ );
407
+ } catch (error) {
408
+ printUsage();
409
+ process.stderr.write(
410
+ `${error instanceof Error ? error.message : "Grok search failed"}\n`,
411
+ );
412
+ process.exitCode = 1;
413
+ }
414
+ }
@@ -0,0 +1,44 @@
1
+ // Dispatcher activation state machine and tool activation helpers.
2
+ // State: inactive -> pending -> active -> draining -> inactive.
3
+ // Tool activation always starts from the current active set so unrelated
4
+ // tools are preserved; deactivation removes only the dispatcher name.
5
+ export type ActivationState = "inactive" | "pending" | "active" | "draining";
6
+
7
+ export class Activation {
8
+ state: ActivationState = "inactive";
9
+
10
+ isActive(): boolean {
11
+ return this.state === "active";
12
+ }
13
+
14
+ /** inactive -> pending (an eligible stage began). */
15
+ request(): boolean {
16
+ if (this.state !== "inactive") return false;
17
+ this.state = "pending";
18
+ return true;
19
+ }
20
+
21
+ /** pending -> active (stage verified). */
22
+ activate(): boolean {
23
+ if (this.state !== "pending") return false;
24
+ this.state = "active";
25
+ return true;
26
+ }
27
+
28
+ /** One idempotent drain step: active -> inactive directly (P-006 unified drain). */
29
+ drain(): boolean {
30
+ if (this.state === "inactive" || this.state === "pending") return false;
31
+ this.state = "inactive";
32
+ return true;
33
+ }
34
+ }
35
+
36
+ /** Additively enable a tool name, preserving every currently active tool. */
37
+ export function activateTool(active: string[], name: string): string[] {
38
+ return [...new Set([...active, name])];
39
+ }
40
+
41
+ /** Remove only the given tool name from the active set. */
42
+ export function deactivateTool(active: string[], name: string): string[] {
43
+ return active.filter((n) => n !== name);
44
+ }
@@ -0,0 +1,54 @@
1
+ // Immutable package-owned professional Agent registry. Files are loaded
2
+ // relative to this module and are never discovered from user or project Agent
3
+ // directories. The registry exposes no override or search-path API.
4
+ import { createHash } from "node:crypto";
5
+ import { readFileSync, statSync } from "node:fs";
6
+ import { dirname, join, resolve } from "node:path";
7
+ import { fileURLToPath } from "node:url";
8
+
9
+ export const AGENT_FILES = [
10
+ "design-explorer.md",
11
+ "contract-reviewer.md",
12
+ "implementation-worker.md",
13
+ "diagnosis-worker.md",
14
+ ] as const;
15
+
16
+ export interface AgentDefinition {
17
+ role: string;
18
+ fileName: string;
19
+ path: string;
20
+ sha256: string;
21
+ bytes: number;
22
+ content: string;
23
+ }
24
+
25
+ export function loadAgentDefinitions(): AgentDefinition[] {
26
+ const agentsDir = resolve(
27
+ dirname(fileURLToPath(import.meta.url)),
28
+ "..",
29
+ "agents",
30
+ );
31
+ return AGENT_FILES.map((fileName) => {
32
+ const path = join(agentsDir, fileName);
33
+ const st = statSync(path);
34
+ if (!st.isFile()) {
35
+ throw new Error(`agent definition is not a regular file: ${path}`);
36
+ }
37
+ const content = readFileSync(path, "utf8");
38
+ return {
39
+ role: fileName.replace(/\.md$/, ""),
40
+ fileName,
41
+ path,
42
+ sha256: createHash("sha256").update(content).digest("hex"),
43
+ bytes: Buffer.byteLength(content, "utf8"),
44
+ content,
45
+ };
46
+ });
47
+ }
48
+
49
+ /** Deterministic role -> content hash map for identity verification. */
50
+ export function agentHashes(): Record<string, string> {
51
+ return Object.fromEntries(
52
+ loadAgentDefinitions().map((a) => [a.role, a.sha256]),
53
+ );
54
+ }