@mrclrchtr/supi-consulting 0.0.0-stage → 8.0.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.md +23 -0
- package/CONTEXT.md +53 -0
- package/README.md +104 -2
- package/docs/adr/0001-use-an-isolated-antigravity-home.md +5 -0
- package/node_modules/@mrclrchtr/supi-core/README.md +137 -0
- package/node_modules/@mrclrchtr/supi-core/package.json +77 -0
- package/node_modules/@mrclrchtr/supi-core/src/api.ts +42 -0
- package/node_modules/@mrclrchtr/supi-core/src/config/config.ts +342 -0
- package/node_modules/@mrclrchtr/supi-core/src/config/prompt-surface.ts +363 -0
- package/node_modules/@mrclrchtr/supi-core/src/config.ts +17 -0
- package/node_modules/@mrclrchtr/supi-core/src/context/context-provider-registry.ts +36 -0
- package/node_modules/@mrclrchtr/supi-core/src/context/context-tag.ts +31 -0
- package/node_modules/@mrclrchtr/supi-core/src/context.ts +8 -0
- package/node_modules/@mrclrchtr/supi-core/src/debug-identity.ts +11 -0
- package/node_modules/@mrclrchtr/supi-core/src/debug-registry.ts +313 -0
- package/node_modules/@mrclrchtr/supi-core/src/debug-timing.ts +120 -0
- package/node_modules/@mrclrchtr/supi-core/src/debug.ts +14 -0
- package/node_modules/@mrclrchtr/supi-core/src/evidence-badge.ts +41 -0
- package/node_modules/@mrclrchtr/supi-core/src/footer-registry.ts +64 -0
- package/node_modules/@mrclrchtr/supi-core/src/index.ts +38 -0
- package/node_modules/@mrclrchtr/supi-core/src/llm.ts +356 -0
- package/node_modules/@mrclrchtr/supi-core/src/model-selection.ts +134 -0
- package/node_modules/@mrclrchtr/supi-core/src/path-utils.ts +44 -0
- package/node_modules/@mrclrchtr/supi-core/src/path.ts +2 -0
- package/node_modules/@mrclrchtr/supi-core/src/project-roots.ts +170 -0
- package/node_modules/@mrclrchtr/supi-core/src/project.ts +15 -0
- package/node_modules/@mrclrchtr/supi-core/src/prompt-surface.ts +4 -0
- package/node_modules/@mrclrchtr/supi-core/src/registry-utils.ts +93 -0
- package/node_modules/@mrclrchtr/supi-core/src/report.ts +121 -0
- package/node_modules/@mrclrchtr/supi-core/src/session-capabilities.ts +242 -0
- package/node_modules/@mrclrchtr/supi-core/src/session-capability-profiles.ts +240 -0
- package/node_modules/@mrclrchtr/supi-core/src/session-utils.ts +71 -0
- package/node_modules/@mrclrchtr/supi-core/src/session.ts +47 -0
- package/node_modules/@mrclrchtr/supi-core/src/settings/settings-registry.ts +105 -0
- package/node_modules/@mrclrchtr/supi-core/src/settings/settings-schema.ts +454 -0
- package/node_modules/@mrclrchtr/supi-core/src/settings.ts +36 -0
- package/node_modules/@mrclrchtr/supi-core/src/spinner-frames.ts +11 -0
- package/node_modules/@mrclrchtr/supi-core/src/status-spinner.ts +68 -0
- package/node_modules/@mrclrchtr/supi-core/src/terminal.ts +60 -0
- package/node_modules/@mrclrchtr/supi-core/src/tui.ts +141 -0
- package/package.json +78 -4
- package/scripts/live-probe.ts +139 -0
- package/src/agents/antigravity/activity.ts +22 -0
- package/src/agents/antigravity/adapter.ts +196 -0
- package/src/agents/antigravity/availability.ts +231 -0
- package/src/agents/antigravity/isolated-home.ts +346 -0
- package/src/agents/antigravity/models.ts +15 -0
- package/src/agents/antigravity/process/environment.ts +33 -0
- package/src/agents/antigravity/process/event-values.ts +273 -0
- package/src/agents/antigravity/process/events.ts +366 -0
- package/src/agents/antigravity/process/hooks.ts +219 -0
- package/src/agents/antigravity/process/ndjson.ts +120 -0
- package/src/agents/antigravity/process/protocol.ts +69 -0
- package/src/agents/antigravity/process/runner.ts +147 -0
- package/src/agents/antigravity/process/subprocess.ts +278 -0
- package/src/agents/antigravity/process/usage.ts +50 -0
- package/src/agents/antigravity/structured-output.ts +58 -0
- package/src/agents/antigravity/types.ts +17 -0
- package/src/agents/errors.ts +27 -0
- package/src/agents/types.ts +43 -0
- package/src/catalogue.ts +17 -0
- package/src/config.ts +26 -0
- package/src/conversation/handles.ts +187 -0
- package/src/evidence-hash.ts +6 -0
- package/src/extension.ts +26 -0
- package/src/footer-constants.ts +5 -0
- package/src/footer.ts +25 -0
- package/src/runtime.ts +244 -0
- package/src/settings.ts +38 -0
- package/src/tool/consulting_run/evidence.ts +169 -0
- package/src/tool/consulting_run/execute.ts +201 -0
- package/src/tool/consulting_run/guidance.ts +3 -0
- package/src/tool/consulting_run/input.ts +117 -0
- package/src/tool/consulting_run/register.ts +36 -0
- package/src/tool/consulting_run/render.ts +261 -0
- package/src/tool/consulting_run/result.ts +185 -0
- package/src/tool/consulting_run/spec.ts +20 -0
- package/src/types.ts +104 -0
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import {
|
|
3
|
+
type SelectItem,
|
|
4
|
+
SelectList,
|
|
5
|
+
type TuiMouseEvent,
|
|
6
|
+
type TuiMouseEventResult,
|
|
7
|
+
truncateToWidth,
|
|
8
|
+
visibleWidth,
|
|
9
|
+
wrapTextWithAnsi,
|
|
10
|
+
} from "@earendil-works/pi-tui";
|
|
11
|
+
|
|
12
|
+
/** The theme methods used by the shared TUI list renderers. */
|
|
13
|
+
export type TuiTheme = Pick<Theme, "fg" | "bold">;
|
|
14
|
+
|
|
15
|
+
/** Settings for one selectable row in a themed list. */
|
|
16
|
+
export interface SelectableRowOptions {
|
|
17
|
+
/** The row label. */
|
|
18
|
+
label: string;
|
|
19
|
+
/** The value shown beside the label. */
|
|
20
|
+
value: string;
|
|
21
|
+
/** The row indent in terminal columns. */
|
|
22
|
+
indent: number;
|
|
23
|
+
/** The largest indent used by the list. */
|
|
24
|
+
maxIndent: number;
|
|
25
|
+
/** The label column width. */
|
|
26
|
+
maxLabelWidth: number;
|
|
27
|
+
/** The row width in terminal columns. */
|
|
28
|
+
width: number;
|
|
29
|
+
/** True when this row is selected. */
|
|
30
|
+
selected: boolean;
|
|
31
|
+
/** Theme used for row colors. */
|
|
32
|
+
theme: TuiTheme;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Settings for a shared action menu. */
|
|
36
|
+
export interface SelectListMenuOptions {
|
|
37
|
+
/** The title shown above the choices. */
|
|
38
|
+
title: string;
|
|
39
|
+
/** The choices shown in the menu. */
|
|
40
|
+
items: SelectItem[];
|
|
41
|
+
/** Theme used for menu colors. */
|
|
42
|
+
theme: TuiTheme;
|
|
43
|
+
/** Called when the user selects a choice. */
|
|
44
|
+
onSelect: (item: SelectItem) => void;
|
|
45
|
+
/** Called when the user closes the menu without a choice. */
|
|
46
|
+
onCancel: () => void;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Component methods for a shared action menu. */
|
|
50
|
+
export interface SelectListMenuComponent {
|
|
51
|
+
render(width: number): string[];
|
|
52
|
+
invalidate(): void;
|
|
53
|
+
handleInput(data: string): void;
|
|
54
|
+
handleMouse(event: TuiMouseEvent): TuiMouseEventResult | undefined;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Build a title line with optional right-side tabs in the shared TUI style. */
|
|
58
|
+
export function formatTuiTitleLine(title: string, theme: TuiTheme, trailing = ""): string {
|
|
59
|
+
const titleText = theme.fg("accent", theme.bold(title));
|
|
60
|
+
return ` ${titleText}${trailing ? ` ${trailing}` : ""}`;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Render one row with a selected marker and an aligned value column. */
|
|
64
|
+
export function renderSelectableRow(options: SelectableRowOptions): string {
|
|
65
|
+
const indent = Math.max(0, options.indent);
|
|
66
|
+
const maxIndent = Math.max(indent, options.maxIndent);
|
|
67
|
+
const maxLabelWidth = Math.max(0, options.maxLabelWidth);
|
|
68
|
+
const width = Math.max(0, options.width);
|
|
69
|
+
const label = truncateToWidth(options.label, maxLabelWidth, "");
|
|
70
|
+
const labelWidth = visibleWidth(label);
|
|
71
|
+
const prefix = options.selected
|
|
72
|
+
? `${" ".repeat(Math.max(0, indent - 2))}${options.theme.fg("accent", "→ ")}`
|
|
73
|
+
: " ".repeat(indent);
|
|
74
|
+
const labelPadding = " ".repeat(maxIndent - indent + maxLabelWidth - labelWidth);
|
|
75
|
+
const valueWidth = Math.max(0, width - maxIndent - maxLabelWidth - 4);
|
|
76
|
+
const value = truncateToWidth(options.value, valueWidth, "");
|
|
77
|
+
const labelText = options.theme.fg(options.selected ? "accent" : "text", label + labelPadding);
|
|
78
|
+
const valueText = options.theme.fg(options.selected ? "accent" : "muted", value);
|
|
79
|
+
|
|
80
|
+
return truncateToWidth(`${prefix}${labelText} ${valueText}`, width);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Render a fixed-height, four-line description preview for a selected row. */
|
|
84
|
+
export function renderDescriptionViewport(
|
|
85
|
+
description: string | undefined,
|
|
86
|
+
width: number,
|
|
87
|
+
): string[] {
|
|
88
|
+
const height = 4;
|
|
89
|
+
const indent = " ";
|
|
90
|
+
const contentWidth = Math.max(1, width - 4);
|
|
91
|
+
const wrapped = description ? wrapTextWithAnsi(description, contentWidth) : [];
|
|
92
|
+
const visible = wrapped.slice(0, height);
|
|
93
|
+
|
|
94
|
+
if (wrapped.length > height) {
|
|
95
|
+
const lastIndex = height - 1;
|
|
96
|
+
visible[lastIndex] = truncateToWidth(`${visible[lastIndex] ?? ""}…`, contentWidth, "…");
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
while (visible.length < height) visible.push("");
|
|
100
|
+
return visible.map((line) => (line ? `${indent}${line}` : ""));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Render a dimmed hint line with the shared separator and truncation rules. */
|
|
104
|
+
export function renderTuiHintLine(hints: string[], theme: TuiTheme, width: number): string {
|
|
105
|
+
return truncateToWidth(theme.fg("dim", hints.join(" · ")), width);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Build the shared title, choices, and key hints for an action menu. */
|
|
109
|
+
export function createSelectListMenu(options: SelectListMenuOptions): SelectListMenuComponent {
|
|
110
|
+
const { items, onSelect, onCancel, theme, title } = options;
|
|
111
|
+
const selectList = new SelectList(items, Math.min(items.length + 2, 15), {
|
|
112
|
+
selectedPrefix: (text) => theme.fg("accent", text),
|
|
113
|
+
selectedText: (text) => theme.fg("accent", text),
|
|
114
|
+
description: (text) => theme.fg("muted", text),
|
|
115
|
+
scrollInfo: (text) => theme.fg("dim", text),
|
|
116
|
+
noMatch: (text) => theme.fg("warning", text),
|
|
117
|
+
});
|
|
118
|
+
selectList.onSelect = onSelect;
|
|
119
|
+
selectList.onCancel = onCancel;
|
|
120
|
+
|
|
121
|
+
return {
|
|
122
|
+
render(width) {
|
|
123
|
+
return [
|
|
124
|
+
truncateToWidth(theme.fg("accent", ` ${title}`), width),
|
|
125
|
+
...selectList.render(width),
|
|
126
|
+
"",
|
|
127
|
+
renderTuiHintLine([" ↑↓ navigate", "Enter select", "Esc cancel"], theme, width),
|
|
128
|
+
];
|
|
129
|
+
},
|
|
130
|
+
invalidate() {
|
|
131
|
+
selectList.invalidate();
|
|
132
|
+
},
|
|
133
|
+
handleInput(data) {
|
|
134
|
+
selectList.handleInput(data);
|
|
135
|
+
},
|
|
136
|
+
handleMouse(event) {
|
|
137
|
+
if (event.y < 1) return undefined;
|
|
138
|
+
return selectList.handleMouse({ ...event, y: event.y - 1 });
|
|
139
|
+
},
|
|
140
|
+
};
|
|
141
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,80 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mrclrchtr/supi-consulting",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "8.0.1",
|
|
4
|
+
"description": "Bounded consultations with explicit agent and workspace selection and observed evidence",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/mrclrchtr/supi.git",
|
|
9
|
+
"directory": "packages/supi-consulting"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://github.com/mrclrchtr/supi/tree/main/packages/supi-consulting#readme",
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/mrclrchtr/supi/issues"
|
|
14
|
+
},
|
|
15
|
+
"publishConfig": {
|
|
16
|
+
"access": "public"
|
|
17
|
+
},
|
|
18
|
+
"keywords": [
|
|
19
|
+
"pi-package",
|
|
20
|
+
"pi",
|
|
21
|
+
"pi-coding-agent",
|
|
22
|
+
"pi-extension",
|
|
23
|
+
"supi",
|
|
24
|
+
"consulting",
|
|
25
|
+
"antigravity",
|
|
26
|
+
"agent",
|
|
27
|
+
"web",
|
|
28
|
+
"workspace"
|
|
29
|
+
],
|
|
30
|
+
"type": "module",
|
|
31
|
+
"files": [
|
|
32
|
+
"src/**/*.ts",
|
|
33
|
+
"scripts/live-probe.ts",
|
|
34
|
+
"README.md",
|
|
35
|
+
"CLAUDE.md",
|
|
36
|
+
"CONTEXT.md",
|
|
37
|
+
"docs/adr/*.md"
|
|
38
|
+
],
|
|
39
|
+
"dependencies": {
|
|
40
|
+
"@mrclrchtr/supi-core": "workspace:*"
|
|
41
|
+
},
|
|
42
|
+
"bundledDependencies": [
|
|
43
|
+
"@mrclrchtr/supi-core"
|
|
44
|
+
],
|
|
45
|
+
"peerDependencies": {
|
|
46
|
+
"@earendil-works/pi-ai": "*",
|
|
47
|
+
"@earendil-works/pi-coding-agent": "*",
|
|
48
|
+
"@earendil-works/pi-tui": "*",
|
|
49
|
+
"typebox": "*"
|
|
50
|
+
},
|
|
51
|
+
"peerDependenciesMeta": {
|
|
52
|
+
"@earendil-works/pi-ai": {
|
|
53
|
+
"optional": true
|
|
54
|
+
},
|
|
55
|
+
"@earendil-works/pi-coding-agent": {
|
|
56
|
+
"optional": true
|
|
57
|
+
},
|
|
58
|
+
"@earendil-works/pi-tui": {
|
|
59
|
+
"optional": true
|
|
60
|
+
},
|
|
61
|
+
"typebox": {
|
|
62
|
+
"optional": true
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
"devDependencies": {
|
|
66
|
+
"@mrclrchtr/supi-test-utils": "workspace:*",
|
|
67
|
+
"@types/node": "25.9.9",
|
|
68
|
+
"vitest": "5.0.4"
|
|
69
|
+
},
|
|
70
|
+
"pi": {
|
|
71
|
+
"extensions": [
|
|
72
|
+
"./src/extension.ts"
|
|
73
|
+
],
|
|
74
|
+
"image": "https://raw.githubusercontent.com/mrclrchtr/supi/main/packages/supi-consulting/assets/social-preview.png"
|
|
75
|
+
},
|
|
76
|
+
"exports": {
|
|
77
|
+
"./extension": "./src/extension.ts",
|
|
78
|
+
"./package.json": "./package.json"
|
|
79
|
+
}
|
|
80
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { cp, lstat, mkdtemp, readdir, readFile, readlink, realpath, rm } from "node:fs/promises";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join, relative } from "node:path";
|
|
5
|
+
import { createAntigravityAdapter } from "../src/agents/antigravity/adapter.ts";
|
|
6
|
+
import { getIsolatedAntigravityPaths } from "../src/agents/antigravity/isolated-home.ts";
|
|
7
|
+
import { isHttpUrl, isSafeWorkspacePath } from "../src/tool/consulting_run/evidence.ts";
|
|
8
|
+
|
|
9
|
+
const LIVE_ENVIRONMENT = "SUPI_CONSULTING_LIVE";
|
|
10
|
+
const MODEL = "gemini-3.8-flash-low";
|
|
11
|
+
const CONTEXT_TOKEN = "supi-consulting-context-token";
|
|
12
|
+
|
|
13
|
+
async function main(): Promise<void> {
|
|
14
|
+
if (process.env[LIVE_ENVIRONMENT] !== "1") {
|
|
15
|
+
throw new Error(`Set ${LIVE_ENVIRONMENT}=1 to run the Consulting live probe.`);
|
|
16
|
+
}
|
|
17
|
+
const paths = getIsolatedAntigravityPaths();
|
|
18
|
+
const adapter = createAntigravityAdapter(paths);
|
|
19
|
+
const availability = await adapter.discover();
|
|
20
|
+
if (availability.status !== "available" || !availability.catalogue.includes(MODEL)) {
|
|
21
|
+
throw new Error("The selected model is not available to the isolated Antigravity account.");
|
|
22
|
+
}
|
|
23
|
+
const consultationWorkspace = await realpath(paths.consultationWorkspace);
|
|
24
|
+
const first = await adapter.execute({
|
|
25
|
+
prompt: `Remember the exact token ${CONTEXT_TOKEN}. Name one current official Node.js documentation page about subprocess cancellation and cite it.`,
|
|
26
|
+
model: MODEL,
|
|
27
|
+
canonicalWorkingDirectory: consultationWorkspace,
|
|
28
|
+
workspaceAccess: false,
|
|
29
|
+
});
|
|
30
|
+
requireWebEvidence(first);
|
|
31
|
+
|
|
32
|
+
const second = await adapter.execute({
|
|
33
|
+
prompt: `What exact context token did I give you? Answer in one sentence and include ${CONTEXT_TOKEN}.`,
|
|
34
|
+
model: MODEL,
|
|
35
|
+
canonicalWorkingDirectory: consultationWorkspace,
|
|
36
|
+
workspaceAccess: false,
|
|
37
|
+
continuation: first.continuation,
|
|
38
|
+
});
|
|
39
|
+
if (!second.answer.answer.includes(CONTEXT_TOKEN)) {
|
|
40
|
+
throw new Error("The follow-up did not retain the Conversation Handle context.");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const fixture = await realpath(await mkdtemp(join(tmpdir(), "supi-consulting-live-")));
|
|
44
|
+
try {
|
|
45
|
+
await cp(join(import.meta.dirname, "../__tests__/fixtures/subprocess-cancellation"), fixture, {
|
|
46
|
+
recursive: true,
|
|
47
|
+
});
|
|
48
|
+
const before = await digestTree(fixture);
|
|
49
|
+
const workspace = await adapter.execute({
|
|
50
|
+
prompt:
|
|
51
|
+
"Inspect the TypeScript subprocess-cancellation fixture. Consult official Node.js documentation and explain the cancellation risk. Return relative file paths within the fixture and source URLs. Do not return absolute file paths.",
|
|
52
|
+
model: MODEL,
|
|
53
|
+
canonicalWorkingDirectory: fixture,
|
|
54
|
+
workspaceAccess: true,
|
|
55
|
+
});
|
|
56
|
+
requireWebEvidence(workspace);
|
|
57
|
+
requireWorkspaceEvidence(workspace, fixture);
|
|
58
|
+
if (!workspace.workspaceUsed) {
|
|
59
|
+
throw new Error("The workspace probe did not observe workspace activity.");
|
|
60
|
+
}
|
|
61
|
+
const after = await digestTree(fixture);
|
|
62
|
+
if (before !== after) throw new Error("The live probe changed the fixture.");
|
|
63
|
+
} finally {
|
|
64
|
+
await rm(fixture, { recursive: true, force: true });
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function requireWebEvidence(result: { webUsed: boolean; answer: { sources: unknown[] } }): void {
|
|
69
|
+
if (!result.webUsed || result.answer.sources.length === 0) {
|
|
70
|
+
throw new Error("The live probe did not observe web evidence.");
|
|
71
|
+
}
|
|
72
|
+
if (
|
|
73
|
+
!result.answer.sources.every(
|
|
74
|
+
(source) => isRecord(source) && typeof source.url === "string" && isHttpUrl(source.url),
|
|
75
|
+
)
|
|
76
|
+
) {
|
|
77
|
+
throw new Error("The live probe returned an invalid source URL.");
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function requireWorkspaceEvidence(
|
|
82
|
+
result: { answer: { workspaceEvidence: unknown[] } },
|
|
83
|
+
workspace: string,
|
|
84
|
+
): void {
|
|
85
|
+
if (
|
|
86
|
+
result.answer.workspaceEvidence.length === 0 ||
|
|
87
|
+
!result.answer.workspaceEvidence.every(
|
|
88
|
+
(item) =>
|
|
89
|
+
isRecord(item) &&
|
|
90
|
+
typeof item.path === "string" &&
|
|
91
|
+
isSafeWorkspacePath(item.path, workspace),
|
|
92
|
+
)
|
|
93
|
+
) {
|
|
94
|
+
throw new Error("The live probe did not return valid workspace evidence.");
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
99
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async function digestTree(directory: string): Promise<string> {
|
|
103
|
+
const hash = createHash("sha256");
|
|
104
|
+
await digestDirectory(directory, directory, hash);
|
|
105
|
+
return hash.digest("hex");
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async function digestDirectory(
|
|
109
|
+
directory: string,
|
|
110
|
+
root: string,
|
|
111
|
+
hash: ReturnType<typeof createHash>,
|
|
112
|
+
): Promise<void> {
|
|
113
|
+
const entries = (await readdir(directory, { withFileTypes: true })).sort((left, right) =>
|
|
114
|
+
left.name.localeCompare(right.name),
|
|
115
|
+
);
|
|
116
|
+
for (const entry of entries) {
|
|
117
|
+
const file = join(directory, entry.name);
|
|
118
|
+
hash.update(relative(root, file));
|
|
119
|
+
if (entry.isDirectory()) {
|
|
120
|
+
hash.update("directory");
|
|
121
|
+
await digestDirectory(file, root, hash);
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
if (entry.isSymbolicLink()) {
|
|
125
|
+
hash.update("link");
|
|
126
|
+
hash.update(await readlink(file));
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
if (entry.isFile()) {
|
|
130
|
+
hash.update("file");
|
|
131
|
+
hash.update(await readFile(file));
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
const info = await lstat(file);
|
|
135
|
+
hash.update(`${info.mode}:${info.size}`);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
await main();
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { normalizeToolName } from "./process/event-values.ts";
|
|
2
|
+
|
|
3
|
+
/** Return whether a successful Antigravity tool name represents web activity. */
|
|
4
|
+
export function isWebToolName(name: string): boolean {
|
|
5
|
+
const normalized = normalizeToolName(name);
|
|
6
|
+
return ["search_web", "read_url_content", "read_url", "web_search"].includes(normalized);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/** Return whether a successful Antigravity tool name represents workspace activity. */
|
|
10
|
+
export function isWorkspaceToolName(name: string): boolean {
|
|
11
|
+
const normalized = normalizeToolName(name);
|
|
12
|
+
return (
|
|
13
|
+
normalized.includes("file") ||
|
|
14
|
+
normalized.includes("directory") ||
|
|
15
|
+
normalized.includes("code_search") ||
|
|
16
|
+
normalized.includes("search_code") ||
|
|
17
|
+
normalized.includes("grep") ||
|
|
18
|
+
normalized.includes("find") ||
|
|
19
|
+
normalized.includes("glob") ||
|
|
20
|
+
["list_files", "list_dir", "read_file"].includes(normalized)
|
|
21
|
+
);
|
|
22
|
+
}
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import type { ConsultationActivity, ConsultationExecutionFacts } from "../../types.ts";
|
|
2
|
+
import { ConsultingAgentError, type ConsultingAgentFailureKind } from "../errors.ts";
|
|
3
|
+
import type {
|
|
4
|
+
ConsultingAgentAdapter,
|
|
5
|
+
ConsultingAgentAvailability,
|
|
6
|
+
ConsultingAgentExecutionOptions,
|
|
7
|
+
ConsultingAvailability,
|
|
8
|
+
} from "../types.ts";
|
|
9
|
+
import { isWebToolName, isWorkspaceToolName } from "./activity.ts";
|
|
10
|
+
import { discoverAntigravityAvailability } from "./availability.ts";
|
|
11
|
+
import {
|
|
12
|
+
getIsolatedAntigravityPaths,
|
|
13
|
+
type IsolatedAntigravityPaths,
|
|
14
|
+
prepareIsolatedAntigravityHome,
|
|
15
|
+
} from "./isolated-home.ts";
|
|
16
|
+
import { probeProjectHooks } from "./process/hooks.ts";
|
|
17
|
+
import { AntigravityProcessError, runAntigravityConversation } from "./process/runner.ts";
|
|
18
|
+
import { ANTIGRAVITY_ANSWER_SCHEMA } from "./structured-output.ts";
|
|
19
|
+
|
|
20
|
+
const MAX_RETAINED_REFERENCE_HASHES = 512;
|
|
21
|
+
const MAX_WARNING_COUNT = 24;
|
|
22
|
+
const MAX_WARNING_CHARS = 500;
|
|
23
|
+
const CONTINUATION_PREFIX = "agy1.";
|
|
24
|
+
|
|
25
|
+
/** The one agent supported by Consulting in this package. */
|
|
26
|
+
export const ANTIGRAVITY_AGENT_ID = "antigravity" as const;
|
|
27
|
+
|
|
28
|
+
/** Compose the private Antigravity implementation behind the neutral adapter. */
|
|
29
|
+
export function createAntigravityAdapter(
|
|
30
|
+
paths: IsolatedAntigravityPaths = getIsolatedAntigravityPaths(),
|
|
31
|
+
): ConsultingAgentAdapter {
|
|
32
|
+
return new AntigravityAdapter(paths);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
class AntigravityAdapter implements ConsultingAgentAdapter {
|
|
36
|
+
readonly identity = ANTIGRAVITY_AGENT_ID;
|
|
37
|
+
readonly consultationWorkspace: string;
|
|
38
|
+
readonly #paths: IsolatedAntigravityPaths;
|
|
39
|
+
|
|
40
|
+
constructor(paths: IsolatedAntigravityPaths) {
|
|
41
|
+
this.#paths = paths;
|
|
42
|
+
this.consultationWorkspace = paths.consultationWorkspace;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async discover(options: { signal?: AbortSignal } = {}): Promise<ConsultingAvailability> {
|
|
46
|
+
const availability = await discoverAntigravityAvailability({
|
|
47
|
+
paths: this.#paths,
|
|
48
|
+
signal: options.signal,
|
|
49
|
+
});
|
|
50
|
+
if (availability.status === "available") {
|
|
51
|
+
const result: ConsultingAgentAvailability = {
|
|
52
|
+
status: "available",
|
|
53
|
+
agent: this.identity,
|
|
54
|
+
agentVersion: availability.cliVersion,
|
|
55
|
+
catalogue: availability.catalogue,
|
|
56
|
+
};
|
|
57
|
+
return Object.freeze({ ...result, catalogue: Object.freeze([...result.catalogue]) });
|
|
58
|
+
}
|
|
59
|
+
return {
|
|
60
|
+
status: "unavailable",
|
|
61
|
+
reason: availability.reason,
|
|
62
|
+
warning: availability.warning,
|
|
63
|
+
...(availability.cliVersion ? { agentVersion: availability.cliVersion } : {}),
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async execute(options: ConsultingAgentExecutionOptions): Promise<ConsultationExecutionFacts> {
|
|
68
|
+
try {
|
|
69
|
+
return await this.#execute(options);
|
|
70
|
+
} catch (error) {
|
|
71
|
+
throw safeExecutionFailure(error);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async #execute(options: ConsultingAgentExecutionOptions): Promise<ConsultationExecutionFacts> {
|
|
76
|
+
const expectedConversationId = options.continuation
|
|
77
|
+
? decodeContinuation(options.continuation)
|
|
78
|
+
: undefined;
|
|
79
|
+
await prepareIsolatedAntigravityHome(this.#paths);
|
|
80
|
+
const hookWarning = options.workspaceAccess
|
|
81
|
+
? await inspectProjectHooks(this.#paths, options, options.onWarning)
|
|
82
|
+
: undefined;
|
|
83
|
+
const rawFacts = await runAntigravityConversation({
|
|
84
|
+
paths: this.#paths,
|
|
85
|
+
cwd: options.canonicalWorkingDirectory,
|
|
86
|
+
prompt: options.prompt,
|
|
87
|
+
model: options.model,
|
|
88
|
+
...(expectedConversationId ? { conversationId: expectedConversationId } : {}),
|
|
89
|
+
...(options.workspaceAccess ? { workspaceDirectory: options.canonicalWorkingDirectory } : {}),
|
|
90
|
+
schema: ANTIGRAVITY_ANSWER_SCHEMA as Record<string, unknown>,
|
|
91
|
+
signal: options.signal,
|
|
92
|
+
onActivity: options.onProgress,
|
|
93
|
+
onProcessStart: options.onProcessStart,
|
|
94
|
+
});
|
|
95
|
+
if (expectedConversationId && rawFacts.conversationId !== expectedConversationId) {
|
|
96
|
+
throw new ConsultingAgentError("conversation-mismatch");
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const warnings = hookWarning ? [hookWarning] : [];
|
|
100
|
+
return {
|
|
101
|
+
answer: rawFacts.answer,
|
|
102
|
+
continuation: encodeContinuation(rawFacts.conversationId),
|
|
103
|
+
...(rawFacts.usage ? { usage: rawFacts.usage } : {}),
|
|
104
|
+
observedActivities: normalizeObservedActivities(rawFacts.observedToolCounts),
|
|
105
|
+
activityCounts: normalizeActivityCounts(rawFacts.observedToolCounts),
|
|
106
|
+
webUsed: rawFacts.successfulToolNames.some(isWebToolName),
|
|
107
|
+
workspaceUsed: rawFacts.successfulToolNames.some(isWorkspaceToolName),
|
|
108
|
+
permissionDenials: rawFacts.permissionDenials,
|
|
109
|
+
observedSourceHashes: rawFacts.observedSourceHashes.slice(0, MAX_RETAINED_REFERENCE_HASHES),
|
|
110
|
+
observedWorkspacePathHashes: rawFacts.observedWorkspacePathHashes.slice(
|
|
111
|
+
0,
|
|
112
|
+
MAX_RETAINED_REFERENCE_HASHES,
|
|
113
|
+
),
|
|
114
|
+
warnings: boundWarnings(warnings),
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async function inspectProjectHooks(
|
|
120
|
+
paths: IsolatedAntigravityPaths,
|
|
121
|
+
options: ConsultingAgentExecutionOptions,
|
|
122
|
+
onWarning: ConsultingAgentExecutionOptions["onWarning"],
|
|
123
|
+
): Promise<string | undefined> {
|
|
124
|
+
const probe = await probeProjectHooks({
|
|
125
|
+
paths,
|
|
126
|
+
cwd: options.canonicalWorkingDirectory,
|
|
127
|
+
signal: options.signal,
|
|
128
|
+
onProcessStart: options.onProcessStart,
|
|
129
|
+
});
|
|
130
|
+
if (!probe.warning) return undefined;
|
|
131
|
+
const warning = boundWarning(probe.warning);
|
|
132
|
+
onWarning?.(warning);
|
|
133
|
+
return warning;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function normalizeObservedActivities(counts: Record<string, number>): ConsultationActivity[] {
|
|
137
|
+
const activities = new Set<ConsultationActivity>();
|
|
138
|
+
for (const name of Object.keys(counts)) activities.add(classifyActivity(name));
|
|
139
|
+
return [...activities];
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function normalizeActivityCounts(
|
|
143
|
+
counts: Record<string, number>,
|
|
144
|
+
): Record<ConsultationActivity, number> {
|
|
145
|
+
const result: Record<ConsultationActivity, number> = { web: 0, workspace: 0, other: 0 };
|
|
146
|
+
for (const [name, count] of Object.entries(counts)) {
|
|
147
|
+
result[classifyActivity(name)] += Math.max(0, Math.floor(count));
|
|
148
|
+
}
|
|
149
|
+
return result;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function classifyActivity(name: string): ConsultationActivity {
|
|
153
|
+
if (isWebToolName(name)) return "web";
|
|
154
|
+
if (isWorkspaceToolName(name)) return "workspace";
|
|
155
|
+
return "other";
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function boundWarnings(warnings: readonly string[]): string[] {
|
|
159
|
+
return [...new Set(warnings.map(boundWarning).filter(Boolean))].slice(0, MAX_WARNING_COUNT);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function boundWarning(warning: string): string {
|
|
163
|
+
return warning.replace(/\s+/g, " ").trim().slice(0, MAX_WARNING_CHARS);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function encodeContinuation(conversationId: string): string {
|
|
167
|
+
return `${CONTINUATION_PREFIX}${Buffer.from(conversationId, "utf8").toString("base64url")}`;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function decodeContinuation(value: string): string {
|
|
171
|
+
if (!value.startsWith(CONTINUATION_PREFIX) || value.length > 1_000) {
|
|
172
|
+
throw new ConsultingAgentError("invalid-continuation");
|
|
173
|
+
}
|
|
174
|
+
const encoded = value.slice(CONTINUATION_PREFIX.length);
|
|
175
|
+
const conversationId = Buffer.from(encoded, "base64url").toString("utf8");
|
|
176
|
+
if (!conversationId || Buffer.from(conversationId, "utf8").toString("base64url") !== encoded) {
|
|
177
|
+
throw new ConsultingAgentError("invalid-continuation");
|
|
178
|
+
}
|
|
179
|
+
return conversationId;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** Drop raw diagnostics on every failure path, including preparation and callbacks. */
|
|
183
|
+
function safeExecutionFailure(error: unknown): ConsultingAgentError {
|
|
184
|
+
if (error instanceof ConsultingAgentError) return new ConsultingAgentError(error.kind);
|
|
185
|
+
const processKinds: Record<AntigravityProcessError["kind"], ConsultingAgentFailureKind> = {
|
|
186
|
+
missing: "unavailable",
|
|
187
|
+
timeout: "timeout",
|
|
188
|
+
cancelled: "cancelled",
|
|
189
|
+
stream: "invalid-response",
|
|
190
|
+
protocol: "invalid-response",
|
|
191
|
+
process: "execution",
|
|
192
|
+
};
|
|
193
|
+
return new ConsultingAgentError(
|
|
194
|
+
error instanceof AntigravityProcessError ? processKinds[error.kind] : "execution",
|
|
195
|
+
);
|
|
196
|
+
}
|