@xynogen/pix-core 0.2.4 → 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.
Files changed (38) hide show
  1. package/README.md +24 -21
  2. package/package.json +11 -17
  3. package/skills/ask-user/SKILL.md +0 -48
  4. package/src/commands/agent-sop/agent-sop.ts +0 -58
  5. package/src/commands/clear/clear.ts +0 -32
  6. package/src/commands/diff/diff.ts +0 -32
  7. package/src/commands/models/models.test.ts +0 -95
  8. package/src/commands/models/models.ts +0 -367
  9. package/src/commands/models/patch-builtin.test.ts +0 -66
  10. package/src/commands/models/patch-builtin.ts +0 -120
  11. package/src/commands/tools.test.ts +0 -15
  12. package/src/commands/update/update.test.ts +0 -112
  13. package/src/commands/update/update.ts +0 -271
  14. package/src/index.ts +0 -45
  15. package/src/lib/data.ts +0 -33
  16. package/src/nudge/capability.test.ts +0 -258
  17. package/src/nudge/capability.ts +0 -189
  18. package/src/nudge/index.ts +0 -17
  19. package/src/nudge/tools.test.ts +0 -157
  20. package/src/nudge/tools.ts +0 -212
  21. package/src/tool/ask/ask.test.ts +0 -243
  22. package/src/tool/ask/components.ts +0 -55
  23. package/src/tool/ask/helpers.ts +0 -77
  24. package/src/tool/ask/index.ts +0 -130
  25. package/src/tool/ask/questionnaire.ts +0 -693
  26. package/src/tool/ask/rpc.ts +0 -84
  27. package/src/tool/ask/schema.ts +0 -69
  28. package/src/tool/ask/single-select-layout.test.ts +0 -124
  29. package/src/tool/ask/single-select-layout.ts +0 -237
  30. package/src/tool/ask/types.ts +0 -17
  31. package/src/tool/todo/todo.test.ts +0 -646
  32. package/src/tool/todo/todo.ts +0 -218
  33. package/src/tool/toolbox/toolbox.test.ts +0 -314
  34. package/src/tool/toolbox/toolbox.ts +0 -570
  35. package/src/ui/diagnostics.ts +0 -145
  36. package/src/ui/footer.ts +0 -513
  37. package/src/ui/welcome.test.ts +0 -124
  38. package/src/ui/welcome.ts +0 -369
@@ -1,271 +0,0 @@
1
- import type {
2
- ExtensionAPI,
3
- ExtensionCommandContext,
4
- } from "@earendil-works/pi-coding-agent";
5
- // ─── Pure logic (exported for tests) ─────────────────────────────────────────
6
-
7
- export const PACKAGE_NAME = "@earendil-works/pi-coding-agent";
8
-
9
- export const TRANSIENT_PATTERNS = [
10
- /eai_again/i,
11
- /etimedout/i,
12
- /econnreset/i,
13
- /econnrefused/i,
14
- /socket hang up/i,
15
- /network/i,
16
- /timeout/i,
17
- /temporar/i,
18
- /too many requests/i,
19
- /\b429\b/,
20
- /\b502\b/,
21
- /\b503\b/,
22
- /\b504\b/,
23
- ];
24
-
25
- export type InstallMethod = "vp" | "bun" | "npm" | "brew" | "native";
26
-
27
- export type CommandSpec = {
28
- command: string;
29
- args: string[];
30
- label: string;
31
- };
32
-
33
- export function isTransient(output: string): boolean {
34
- return TRANSIENT_PATTERNS.some((pattern) => pattern.test(output));
35
- }
36
-
37
- export function commandFor(method: InstallMethod): CommandSpec | undefined {
38
- switch (method) {
39
- case "vp":
40
- return {
41
- command: "vp",
42
- args: ["add", "-g", `${PACKAGE_NAME}@latest`],
43
- label: `vp add -g ${PACKAGE_NAME}@latest`,
44
- };
45
- case "bun":
46
- return {
47
- command: "bun",
48
- args: ["add", "-g", `${PACKAGE_NAME}@latest`],
49
- label: `bun add -g ${PACKAGE_NAME}@latest`,
50
- };
51
- case "npm":
52
- return {
53
- command: "npm",
54
- args: ["install", "-g", `${PACKAGE_NAME}@latest`],
55
- label: `npm install -g ${PACKAGE_NAME}@latest`,
56
- };
57
- case "brew":
58
- return {
59
- command: "/bin/sh",
60
- args: ["-lc", "brew upgrade pi-coding-agent || brew upgrade pi"],
61
- label: "brew upgrade pi-coding-agent || brew upgrade pi",
62
- };
63
- case "native":
64
- return undefined;
65
- }
66
- }
67
-
68
- export function formatUpdateSummary(
69
- before: string,
70
- after: string,
71
- attempts: number,
72
- ): string {
73
- const changed =
74
- before !== after && before !== "unknown" && after !== "unknown";
75
- const summary = changed
76
- ? `Pi updated: ${before} → ${after}`
77
- : `Pi is up to date (${after}).`;
78
- return attempts > 1
79
- ? `${summary} Retried ${attempts - 1} transient failure(s).`
80
- : summary;
81
- }
82
-
83
- async function resolveCommand(command: string, pi: ExtensionAPI) {
84
- const result = await pi.exec(
85
- "/bin/sh",
86
- ["-lc", `command -v ${command} || true`],
87
- { timeout: 10_000 },
88
- );
89
- return result.stdout.trim().split("\n")[0] || undefined;
90
- }
91
-
92
- async function currentVersion(pi: ExtensionAPI) {
93
- const result = await pi.exec("pi", ["--version"], { timeout: 10_000 });
94
- return result.stdout.trim() || result.stderr.trim() || "unknown";
95
- }
96
-
97
- async function detectInstallMethod(pi: ExtensionAPI): Promise<InstallMethod> {
98
- const piPath = await resolveCommand("pi", pi);
99
- const realPiPath = piPath
100
- ? (
101
- await pi.exec(
102
- "/bin/sh",
103
- ["-lc", `realpath ${piPath} 2>/dev/null || printf %s ${piPath}`],
104
- { timeout: 10_000 },
105
- )
106
- ).stdout.trim()
107
- : undefined;
108
-
109
- if (piPath?.includes("/.vite-plus/") || realPiPath?.includes("/.vite-plus/"))
110
- return "vp";
111
- if (piPath?.includes("/.bun/") || realPiPath?.includes("/.bun/"))
112
- return "bun";
113
- if (
114
- piPath?.includes("/Homebrew/") ||
115
- piPath?.includes("/homebrew/") ||
116
- realPiPath?.includes("/Homebrew/") ||
117
- realPiPath?.includes("/homebrew/")
118
- )
119
- return "brew";
120
-
121
- if (piPath) {
122
- const hasGlobalNpm = await pi.exec(
123
- "/bin/sh",
124
- [
125
- "-lc",
126
- `p=${piPath}; i=0; while [ $i -lt 5 ]; do d=$(dirname "$p"); [ -d "$d/node_modules/${PACKAGE_NAME}" ] && exit 0; p=$d; i=$((i+1)); done; exit 1`,
127
- ],
128
- { timeout: 10_000 },
129
- );
130
- if ((hasGlobalNpm.code ?? 1) === 0) return "npm";
131
- }
132
-
133
- if (await resolveCommand("vp", pi)) return "vp";
134
- if (await resolveCommand("bun", pi)) return "bun";
135
- if (await resolveCommand("npm", pi)) return "npm";
136
- if (await resolveCommand("brew", pi)) return "brew";
137
- return "native";
138
- }
139
-
140
- async function runWithRetry(pi: ExtensionAPI, spec: CommandSpec) {
141
- let lastOutput = "";
142
- for (let attempt = 1; attempt <= 3; attempt++) {
143
- const result = await pi.exec(spec.command, spec.args, { timeout: 180_000 });
144
- lastOutput = [result.stdout, result.stderr]
145
- .filter(Boolean)
146
- .join("\n")
147
- .trim();
148
- if ((result.code ?? 0) === 0)
149
- return { ok: true, output: lastOutput, attempts: attempt };
150
- if (attempt === 3 || !isTransient(lastOutput))
151
- return { ok: false, output: lastOutput, attempts: attempt };
152
- await new Promise((resolve) => setTimeout(resolve, attempt * 1500));
153
- }
154
- return { ok: false, output: lastOutput, attempts: 3 };
155
- }
156
-
157
- async function updatePi(pi: ExtensionAPI, ctx: ExtensionCommandContext) {
158
- await (
159
- ctx as ExtensionCommandContext & { waitForIdle?: () => Promise<void> }
160
- ).waitForIdle?.();
161
-
162
- const before = await currentVersion(pi).catch(() => "unknown");
163
- const method = await detectInstallMethod(pi);
164
- const spec = commandFor(method);
165
-
166
- if (!spec) {
167
- ctx.ui.notify(
168
- `Pi ${before}; install method appears native. Please update the native binary manually.`,
169
- "warning",
170
- );
171
- return;
172
- }
173
-
174
- ctx.ui.notify(`Updating Pi via ${method}: ${spec.label}`, "info");
175
- const result = await runWithRetry(pi, spec);
176
- const after = await currentVersion(pi).catch(() => "unknown");
177
-
178
- if (!result.ok) {
179
- ctx.ui.notify(
180
- `Pi update failed after ${result.attempts} attempt(s). ${result.output || "No output."}`,
181
- "error",
182
- );
183
- return;
184
- }
185
-
186
- ctx.ui.notify(formatUpdateSummary(before, after, result.attempts), "info");
187
- }
188
-
189
- async function updateExtensions(
190
- pi: ExtensionAPI,
191
- ctx: ExtensionCommandContext,
192
- ) {
193
- ctx.ui.notify("Updating Pi extensions from dotfiles setup", "info");
194
- const result = await pi.exec(
195
- "/bin/sh",
196
- ["-lc", '"$HOME/dotfiles/ai_config/pi/setup.sh"'],
197
- { timeout: 240_000 },
198
- );
199
- const output = [result.stdout, result.stderr]
200
- .filter(Boolean)
201
- .join("\n")
202
- .trim();
203
- if ((result.code ?? 0) !== 0) {
204
- ctx.ui.notify(
205
- `Pi extensions update failed. ${output || "No output."}`,
206
- "error",
207
- );
208
- return;
209
- }
210
- ctx.ui.notify(
211
- "Pi extensions updated. Please run /reload to apply changes.",
212
- "warning",
213
- );
214
- }
215
-
216
- async function updatePackages(pi: ExtensionAPI, ctx: ExtensionCommandContext) {
217
- ctx.ui.notify("Updating pi packages (pi update --extensions)", "info");
218
- const result = await pi.exec("pi", ["update", "--extensions"], {
219
- timeout: 240_000,
220
- });
221
- const output = [result.stdout, result.stderr]
222
- .filter(Boolean)
223
- .join("\n")
224
- .trim();
225
- if ((result.code ?? 0) !== 0) {
226
- ctx.ui.notify(
227
- `Pi package update failed. ${output || "No output."}`,
228
- "error",
229
- );
230
- return;
231
- }
232
- ctx.ui.notify(
233
- "Pi packages updated. Please run /reload to apply changes.",
234
- "warning",
235
- );
236
- }
237
-
238
- async function updateAll(pi: ExtensionAPI, ctx: ExtensionCommandContext) {
239
- await updatePi(pi, ctx);
240
- await updateExtensions(pi, ctx);
241
- await updatePackages(pi, ctx);
242
- }
243
-
244
- export default function (pi: ExtensionAPI) {
245
- (
246
- pi as ExtensionAPI & {
247
- registerFlag: (name: string, opts: unknown) => void;
248
- }
249
- ).registerFlag("update", {
250
- description: "Update Pi, dotfiles extensions, and pi packages",
251
- type: "boolean",
252
- default: false,
253
- });
254
-
255
- pi.registerCommand("update", {
256
- description: "Update Pi, dotfiles extensions, and pi packages",
257
- handler: async (_args, ctx) => {
258
- await updateAll(pi, ctx);
259
- },
260
- });
261
-
262
- pi.on("session_start", async (_event, ctx) => {
263
- const flags = pi as ExtensionAPI & {
264
- getFlag?: (name: string) => boolean;
265
- sendUserMessage?: (message: string, opts?: unknown) => void;
266
- };
267
- if (!flags.getFlag?.("update")) return;
268
- flags.sendUserMessage?.("/update", { deliverAs: "followUp" });
269
- ctx.ui.notify("Queued /update from --update", "info");
270
- });
271
- }
package/src/index.ts DELETED
@@ -1,45 +0,0 @@
1
- /**
2
- * pix-core — Pi extension bundle
3
- *
4
- * Layout (grouped by concern):
5
- * - ui/ — welcome (π banner + health checks), footer (status bar)
6
- * - commands/ — models (/models picker), update (/update self-update),
7
- * diff (/diff)
8
- * - tool/ — todo (durable execution checklist),
9
- * toolbox (/toolbox command — user toggles tools on/off),
10
- * lazy (lazy tool exposure — gates schemas out of the prompt)
11
- * - nudge/ — model-steering reminders (tools / capability+skills)
12
- * - lib/ — shared data layer (models.dev + BenchLM)
13
- *
14
- * Depends on pix-data (github.com/xynogen/pix-data) for shared
15
- * models.dev + BenchLM cache at ~/.cache/pi/.
16
- */
17
-
18
- import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
19
- import registerAgentSop from "./commands/agent-sop/agent-sop.ts";
20
- import registerClear from "./commands/clear/clear.ts";
21
- import registerDiff from "./commands/diff/diff.ts";
22
- import registerModels from "./commands/models/models.ts";
23
- import registerUpdate from "./commands/update/update.ts";
24
- import registerNudges from "./nudge/index.ts";
25
- import registerAsk from "./tool/ask/index.ts";
26
- import registerTodo from "./tool/todo/todo.ts";
27
- import registerToolbox from "./tool/toolbox/toolbox.ts";
28
- import registerDiagnostics from "./ui/diagnostics.ts";
29
- import registerFooter from "./ui/footer.ts";
30
- import registerWelcome from "./ui/welcome.ts";
31
-
32
- export default function (pi: ExtensionAPI): void {
33
- registerAgentSop(pi);
34
- registerWelcome(pi);
35
- registerFooter(pi);
36
- registerDiagnostics(pi);
37
- registerModels(pi);
38
- registerUpdate(pi);
39
- registerDiff(pi);
40
- registerClear(pi);
41
- registerTodo(pi);
42
- registerAsk(pi);
43
- registerToolbox(pi);
44
- registerNudges(pi);
45
- }
package/src/lib/data.ts DELETED
@@ -1,33 +0,0 @@
1
- /**
2
- * data.ts — model data layer (shim)
3
- *
4
- * Thin re-export of the shared data layer from @xynogen/pix-data
5
- * (github.com/xynogen/pix-mono/tree/main/packages/pix-data). Cache lives at
6
- * ~/.cache/pi/ and is shared across all Pi extensions — pix-data warms it on
7
- * session start; this extension reads from it.
8
- *
9
- * Consumers in this extension dir:
10
- * footer.ts — lookupModelsDev, lookupBenchmark, ModelsDevModel
11
- * models.ts — lookupModelsDev, lookupBenchmark
12
- */
13
-
14
- export type {
15
- BenchmarkEntry,
16
- ModelsDevApi,
17
- ModelsDevModel,
18
- } from "../../../pix-data/src/index.ts";
19
- export {
20
- benchmark,
21
- buildModelsDevIndex,
22
- CACHE_DIR,
23
- DataSource,
24
- fetchModelsDevIndex,
25
- lookupBenchmark,
26
- lookupInIndex,
27
- lookupModelsDev,
28
- modelsDev,
29
- } from "../../../pix-data/src/index.ts";
30
-
31
- export default function (_pi: unknown): void {
32
- // pix-data warms this cache on startup — nothing to do here.
33
- }
@@ -1,258 +0,0 @@
1
- import { describe, expect, test } from "bun:test";
2
- import { mkdirSync, rmSync, writeFileSync } from "node:fs";
3
- import { join } from "node:path";
4
- import registerCapabilityNudge, {
5
- buildOrientation,
6
- CAPABILITY_REMINDER,
7
- countInvocableSkills,
8
- graphifyHint,
9
- partitionTools,
10
- } from "./capability.ts";
11
-
12
- // Minimal Skill-shaped fixtures (only fields the builder reads).
13
- const skill = (
14
- name: string,
15
- description: string,
16
- disableModelInvocation = false,
17
- ) =>
18
- ({
19
- name,
20
- description,
21
- disableModelInvocation,
22
- filePath: "",
23
- baseDir: "",
24
- sourceInfo: {} as never,
25
- }) as never;
26
-
27
- // Minimal ToolInfo-shaped fixture.
28
- const tool = (name: string, source: string) =>
29
- ({
30
- name,
31
- description: `${name} desc.`,
32
- parameters: {},
33
- sourceInfo: { source, path: "", scope: "user", origin: "package" },
34
- }) as never;
35
-
36
- describe("CAPABILITY_REMINDER", () => {
37
- test("is terse — fires every turn, must stay cheap", () => {
38
- expect(CAPABILITY_REMINDER.split(/\s+/).length).toBeLessThanOrEqual(28);
39
- });
40
-
41
- test("names the core capability surfaces", () => {
42
- for (const cap of ["skill", "tool", "MCP", "web", "user"]) {
43
- expect(CAPABILITY_REMINDER).toContain(cap);
44
- }
45
- });
46
-
47
- test("steers away from improvising", () => {
48
- expect(CAPABILITY_REMINDER.toLowerCase()).toContain("improvis");
49
- });
50
-
51
- test("nudges model to call read_skills() when a skill matches", () => {
52
- expect(CAPABILITY_REMINDER).toContain("read_skills()");
53
- });
54
-
55
- test("does not mention user-only toolbox command", () => {
56
- expect(CAPABILITY_REMINDER).not.toContain("/toolbox");
57
- expect(CAPABILITY_REMINDER).not.toContain("toolbox(");
58
- });
59
- });
60
-
61
- describe("countInvocableSkills", () => {
62
- test("zero for undefined / empty", () => {
63
- expect(countInvocableSkills(undefined)).toBe(0);
64
- expect(countInvocableSkills([])).toBe(0);
65
- });
66
-
67
- test("excludes user-only skills", () => {
68
- const n = countInvocableSkills([
69
- skill("a", "."),
70
- skill("b", ".", true),
71
- skill("c", "."),
72
- ]);
73
- expect(n).toBe(2);
74
- });
75
- });
76
-
77
- describe("partitionTools", () => {
78
- test("splits MCP-sourced from other tools by source", () => {
79
- const { mcp, other } = partitionTools([
80
- tool("read", "builtin"),
81
- tool("context7-docs", "mcp:context7"),
82
- tool("notion-search", "MCP server notion"),
83
- tool("grep", "extension:pix-core"),
84
- ]);
85
- expect(mcp).toBe(2);
86
- expect(other).toBe(2);
87
- });
88
-
89
- test("handles undefined", () => {
90
- expect(partitionTools(undefined)).toEqual({
91
- mcp: 0,
92
- other: 0,
93
- active: 0,
94
- gated: 0,
95
- });
96
- });
97
-
98
- test("without an active set, every tool counts as active (gated 0)", () => {
99
- const { active, gated } = partitionTools([
100
- tool("read", "builtin"),
101
- tool("find", "builtin"),
102
- ]);
103
- expect(active).toBe(2);
104
- expect(gated).toBe(0);
105
- });
106
-
107
- test("with an active set, splits active vs gated", () => {
108
- const { active, gated } = partitionTools(
109
- [
110
- tool("read", "builtin"),
111
- tool("grep", "builtin"),
112
- tool("find", "builtin"),
113
- tool("ls", "builtin"),
114
- ],
115
- ["read", "grep"],
116
- );
117
- expect(active).toBe(2);
118
- expect(gated).toBe(2);
119
- });
120
- });
121
-
122
- describe("buildOrientation", () => {
123
- test("summarizes counts of tools, MCP tools, and skills", () => {
124
- const out = buildOrientation(
125
- [tool("read", "builtin"), tool("ctx", "mcp:context7")],
126
- [skill("commit", "Commit changes."), skill("plan", "Plan work.")],
127
- );
128
- expect(out).toContain("1 tool");
129
- expect(out).toContain("1 MCP tool");
130
- expect(out).toContain("2 skills");
131
- });
132
-
133
- test("explains how to use read_skills() without user-only toolbox", () => {
134
- const out = buildOrientation([tool("read", "builtin")], []);
135
- expect(out).toContain("read_skills()");
136
- expect(out).not.toContain("/toolbox");
137
- expect(out).not.toContain("toolbox(");
138
- });
139
-
140
- test("calls out gated tools and points at toolbox to enable them", () => {
141
- const out = buildOrientation(
142
- [
143
- tool("read", "builtin"),
144
- tool("grep", "builtin"),
145
- tool("find", "builtin"),
146
- tool("ls", "builtin"),
147
- ],
148
- [],
149
- ["read", "grep"], // active set: 2 gated out
150
- );
151
- expect(out).toContain("2 are gated");
152
- expect(out).toContain("function definitions");
153
- });
154
-
155
- test("singular phrasing when exactly one tool is gated", () => {
156
- const out = buildOrientation(
157
- [tool("read", "builtin"), tool("find", "builtin")],
158
- [],
159
- ["read"],
160
- );
161
- expect(out).toContain("1 is gated");
162
- expect(out).toContain("function definitions");
163
- });
164
-
165
- test("no gate line when nothing is gated", () => {
166
- const out = buildOrientation(
167
- [tool("read", "builtin"), tool("grep", "builtin")],
168
- [],
169
- ["read", "grep"],
170
- );
171
- expect(out).not.toContain("gated out of the prompt");
172
- });
173
-
174
- test("no gate line when active set is unknown", () => {
175
- const out = buildOrientation(
176
- [tool("read", "builtin"), tool("find", "builtin")],
177
- [],
178
- );
179
- expect(out).not.toContain("gated out of the prompt");
180
- });
181
-
182
- test("lists invocable skill names, sorted, excluding user-only", () => {
183
- const out = buildOrientation(
184
- [],
185
- [skill("zebra", "z."), skill("alpha", "a."), skill("hidden", "h.", true)],
186
- );
187
- expect(out).toContain("Skills: alpha, zebra.");
188
- expect(out).not.toContain("hidden");
189
- });
190
-
191
- test("omits the skills line when no invocable skills", () => {
192
- const out = buildOrientation(
193
- [tool("read", "builtin")],
194
- [skill("x", ".", true)],
195
- );
196
- expect(out).not.toContain("Skills:");
197
- });
198
-
199
- test("steers away from improvising", () => {
200
- const out = buildOrientation([tool("read", "builtin")], []);
201
- expect(out.toLowerCase()).toContain("improvis");
202
- });
203
-
204
- test("frames the block as non-actionable so the model acts on the prompt", () => {
205
- const out = buildOrientation([tool("read", "builtin")], []);
206
- const last = out.trim().split("\n").at(-1) ?? "";
207
- expect(last.toLowerCase()).toContain("not a task");
208
- expect(last.toLowerCase()).toContain("do not reply");
209
- });
210
- });
211
-
212
- describe("registerCapabilityNudge", () => {
213
- test("injects orientation into system prompt, not a custom message", async () => {
214
- let handler: ((event: { systemPrompt?: string }) => unknown) | undefined;
215
- const pi = {
216
- on(event: string, fn: typeof handler) {
217
- if (event === "before_agent_start") handler = fn;
218
- },
219
- getAllTools() {
220
- return [tool("read", "builtin")];
221
- },
222
- getActiveTools() {
223
- return ["read"];
224
- },
225
- } as never;
226
-
227
- registerCapabilityNudge(pi);
228
- const result = (await handler?.({ systemPrompt: "BASE" })) as {
229
- systemPrompt?: string;
230
- message?: unknown;
231
- };
232
-
233
- expect(result.message).toBeUndefined();
234
- expect(result.systemPrompt).toStartWith("BASE\n\nToolbelt:");
235
- expect(result.systemPrompt).toContain("Orientation only");
236
- });
237
- });
238
-
239
- describe("graphifyHint", () => {
240
- const tmpDir = join(import.meta.dir, ".graphify-hint-test-tmp");
241
-
242
- test("returns undefined when graphify-out/graph.json absent", () => {
243
- expect(graphifyHint(tmpDir)).toBeUndefined();
244
- });
245
-
246
- test("returns hint string when graphify-out/graph.json exists", () => {
247
- try {
248
- mkdirSync(join(tmpDir, "graphify-out"), { recursive: true });
249
- writeFileSync(join(tmpDir, "graphify-out", "graph.json"), "{}");
250
- const hint = graphifyHint(tmpDir);
251
- expect(hint).toBeTypeOf("string");
252
- expect(hint).toContain("graphify");
253
- expect(hint).toContain("graphify query");
254
- } finally {
255
- rmSync(tmpDir, { recursive: true, force: true });
256
- }
257
- });
258
- });