metergraph-cli 0.2.3 → 0.2.4
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 +78 -14
- package/assets/skill/SKILL.md +124 -31
- package/assets/skill/manifest.json +3 -3
- package/assets/skills/manifest.json +31 -31
- package/assets/skills/metergraph-analyze/SKILL.md +161 -58
- package/assets/skills/metergraph-candidates/SKILL.md +50 -8
- package/assets/skills/metergraph-evals/SKILL.md +83 -16
- package/assets/skills/metergraph-investigate/SKILL.md +90 -34
- package/assets/skills/metergraph-iterate/SKILL.md +82 -12
- package/assets/skills/metergraph-local-bundle/SKILL.md +146 -128
- package/assets/skills/metergraph-model-swap/SKILL.md +142 -20
- package/assets/skills/metergraph-onboarding/SKILL.md +93 -49
- package/assets/skills/metergraph-report/SKILL.md +60 -15
- package/assets/skills/metergraph-workloads/SKILL.md +97 -13
- package/native/README.md +85 -0
- package/native/anchored-fs.c +243 -0
- package/native/bin/anchored-fs-darwin-arm64 +0 -0
- package/native/bin/anchored-fs-darwin-x64 +0 -0
- package/native/bin/anchored-fs-linux-arm64 +0 -0
- package/native/bin/anchored-fs-linux-x64 +0 -0
- package/native/build.mjs +55 -0
- package/native/manifest.json +9 -0
- package/native/targets.mjs +24 -0
- package/package.json +10 -3
- package/src/action-help.js +130 -0
- package/src/agent-key.js +21 -14
- package/src/anchored-fs.js +163 -0
- package/src/args.js +125 -17
- package/src/auth-binding.js +12 -7
- package/src/auth-callback.js +10 -2
- package/src/auth-login.js +111 -28
- package/src/auth-oauth.js +89 -45
- package/src/auth-session.js +19 -12
- package/src/auth-store.js +28 -7
- package/src/cli.js +52 -8
- package/src/constants.js +37 -9
- package/src/experiments-output.js +7 -6
- package/src/experiments.js +26 -17
- package/src/output.js +274 -79
- package/src/read-contract.js +42 -8
- package/src/read-output.js +66 -13
- package/src/read.js +90 -15
- package/src/repository-identity.js +2 -2
- package/src/setup-env-git.js +1 -1
- package/src/setup-env.js +16 -9
- package/src/setup-interactive.js +19 -4
- package/src/setup-refusal.js +2 -2
- package/src/setup-state.js +9 -4
- package/src/setup.js +15 -3
- package/src/skill-bundle.js +2 -2
- package/src/skill-trusted.js +56 -0
- package/src/skill.js +28 -9
- package/src/style.js +33 -7
- package/src/tools-output.js +73 -0
- package/src/tools.js +249 -0
- package/src/trace-contract.js +1 -1
- package/src/transport.js +21 -0
- package/src/verify-output.js +2 -2
- package/src/verify.js +5 -2
package/src/setup-refusal.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { randomBytes, randomUUID } from "node:crypto";
|
|
2
|
-
import fs from "
|
|
2
|
+
import fs, { withRollback } from "./anchored-fs.js";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
|
|
5
5
|
// What the --json --no-browser background waiter is doing with an approval
|
|
@@ -80,7 +80,7 @@ function write(root, value) {
|
|
|
80
80
|
fs.renameSync(temp, fileFor(root));
|
|
81
81
|
return true;
|
|
82
82
|
} catch {
|
|
83
|
-
try { fs.unlinkSync(temp); } catch { /* no temp */ }
|
|
83
|
+
try { withRollback(() => fs.unlinkSync(temp)); } catch { /* no temp */ }
|
|
84
84
|
return false;
|
|
85
85
|
}
|
|
86
86
|
}
|
package/src/setup-state.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { randomBytes, randomUUID } from "node:crypto";
|
|
2
|
-
import fs from "
|
|
2
|
+
import fs, { withRollback } from "./anchored-fs.js";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
|
|
5
|
+
import { parseOrigin } from "./origin.js";
|
|
6
|
+
|
|
5
7
|
import { Stop } from "./auth-store.js";
|
|
6
8
|
|
|
7
9
|
// Public, non-credential project state. It is persisted before browser
|
|
@@ -53,7 +55,7 @@ export function setupReceipt(value) {
|
|
|
53
55
|
function core(value) {
|
|
54
56
|
return value.schema_version === 1 && UUID.test(value.family_id) &&
|
|
55
57
|
(value.key_id === null || UUID.test(value.key_id)) && PHASES.has(value.phase) &&
|
|
56
|
-
typeof value.origin === "string" && typeof value.workspace_id === "string" && UUID.test(value.workspace_id);
|
|
58
|
+
typeof value.origin === "string" && parseOrigin(value.origin) === value.origin && typeof value.workspace_id === "string" && UUID.test(value.workspace_id);
|
|
57
59
|
}
|
|
58
60
|
|
|
59
61
|
function valid(value) {
|
|
@@ -78,6 +80,7 @@ export function readSetupState(root) {
|
|
|
78
80
|
let stat;
|
|
79
81
|
try { stat = fs.lstatSync(file); }
|
|
80
82
|
catch (error) {
|
|
83
|
+
if (error.code === "ENOTSUP") throw new Stop("unsupported", "secure_writes_unavailable");
|
|
81
84
|
if (error.code === "ENOENT") return null;
|
|
82
85
|
throw new Stop("filesystem_error", "setup_state_read_failed");
|
|
83
86
|
}
|
|
@@ -95,6 +98,7 @@ export function readSetupState(root) {
|
|
|
95
98
|
selected_client: null, skill_status: "pending" }) : value, content };
|
|
96
99
|
} catch (error) {
|
|
97
100
|
if (error instanceof Stop) throw error;
|
|
101
|
+
if (error.code === "ENOTSUP") throw new Stop("unsupported", "secure_writes_unavailable");
|
|
98
102
|
throw new Stop("conflict", "setup_state_invalid");
|
|
99
103
|
} finally {
|
|
100
104
|
if (fd !== undefined) fs.closeSync(fd);
|
|
@@ -134,10 +138,11 @@ export function writeSetupState(root, next, previous) {
|
|
|
134
138
|
fs.renameSync(temp, dest);
|
|
135
139
|
} catch (error) {
|
|
136
140
|
if (error instanceof Stop) throw error;
|
|
141
|
+
if (error.code === "ENOTSUP") throw new Stop("unsupported", "secure_writes_unavailable");
|
|
137
142
|
if (error.code === "EEXIST") throw new Stop("conflict", "setup_state_locked");
|
|
138
143
|
throw new Stop("filesystem_error", "setup_state_write_failed");
|
|
139
144
|
} finally {
|
|
140
|
-
try { fs.unlinkSync(temp); } catch { /* no temp */ }
|
|
141
|
-
if (locked) { try { fs.unlinkSync(lock); } catch { /* next run refuses */ } }
|
|
145
|
+
try { withRollback(() => fs.unlinkSync(temp)); } catch { /* no temp */ }
|
|
146
|
+
if (locked) { try { withRollback(() => fs.unlinkSync(lock)); } catch { /* next run refuses */ } }
|
|
142
147
|
}
|
|
143
148
|
}
|
package/src/setup.js
CHANGED
|
@@ -57,6 +57,9 @@ export async function runSetup(options, progress = () => {}) {
|
|
|
57
57
|
const where = options.runtime !== "local" ? { kind: "declared_cloud" } : environment;
|
|
58
58
|
return fail("unsupported", "run_on_local_machine", "not_ready", null, remoteSetupAction(where, options.origin, options));
|
|
59
59
|
}
|
|
60
|
+
if (new URL(options.origin).protocol === "http:") {
|
|
61
|
+
return fail("unsupported", "local_docker_transport_required", "not_ready", null);
|
|
62
|
+
}
|
|
60
63
|
const trap = trapSignals();
|
|
61
64
|
let receipt = null;
|
|
62
65
|
let loginVerified = false;
|
|
@@ -112,9 +115,14 @@ export async function runSetup(options, progress = () => {}) {
|
|
|
112
115
|
// An env key this setup does not own stops before any sign in, so the
|
|
113
116
|
// refusal costs no approval and leaves the binding untouched. The check
|
|
114
117
|
// after sign in stays, against a fresh read.
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
if (
|
|
118
|
+
const early = currentEnvValues(preflightEnv({ project: root, envFile: options.envFile, signal: trap.signal }));
|
|
119
|
+
if (early.token !== null) {
|
|
120
|
+
if (!saved) return stop("conflict", "existing_ingest_key_unowned");
|
|
121
|
+
// The saved public route cannot choose the destination of a token.
|
|
122
|
+
// Require its colocated ingest destination before any sign-in request.
|
|
123
|
+
if (early.ingestUrl !== intendedOrigin && early.ingestUrl !== `${intendedOrigin}/v1/ingest`) {
|
|
124
|
+
return stop("conflict", "ingest_url_mismatch");
|
|
125
|
+
}
|
|
118
126
|
}
|
|
119
127
|
const bindingDiffers = bound !== null && (bound.origin !== intendedOrigin ||
|
|
120
128
|
(intendedWorkspace !== null && bound.workspace_id !== intendedWorkspace));
|
|
@@ -182,6 +190,10 @@ export async function runSetup(options, progress = () => {}) {
|
|
|
182
190
|
// being reused. Pending delivery is acknowledged, including after a
|
|
183
191
|
// previous process stopped after the env write.
|
|
184
192
|
if (previous && existing.token !== null) {
|
|
193
|
+
// Recheck the fresh env snapshot immediately before the bearer sink.
|
|
194
|
+
if (existing.ingestUrl !== origin && existing.ingestUrl !== `${origin}/v1/ingest`) {
|
|
195
|
+
return stop("conflict", "ingest_url_mismatch");
|
|
196
|
+
}
|
|
185
197
|
const checked = await checkCredential(origin, existing.token, previous.value, profile, trap.signal);
|
|
186
198
|
if (checked.ok) {
|
|
187
199
|
if (existing.ingestUrl !== origin && existing.ingestUrl !== `${origin}/v1/ingest`) {
|
package/src/skill-bundle.js
CHANGED
|
@@ -5,7 +5,7 @@ import { readFileSync } from "node:fs";
|
|
|
5
5
|
// public skill at the manifest's source_url. The source has no version of its
|
|
6
6
|
// own, so the revision is derived from the content hash. The hash is pinned
|
|
7
7
|
// here as well as in the manifest, so editing either file alone is detected.
|
|
8
|
-
const PINNED_SHA256 = "
|
|
8
|
+
const PINNED_SHA256 = "f31c24c4074707df0e34502bd9efbfde4fff5703e6fd367e0f60692f9f5878ce";
|
|
9
9
|
|
|
10
10
|
const ASSET_DIR = new URL("../assets/skill/", import.meta.url);
|
|
11
11
|
const MANIFEST_KEYS = ["file", "manifest_version", "name", "revision", "sha256", "size", "source_url"];
|
|
@@ -55,7 +55,7 @@ export function loadBundledSkill() {
|
|
|
55
55
|
// The workflow skills that "skills install" writes, vendored from the public
|
|
56
56
|
// skills repository at one commit. The manifest's own hash is pinned here, and
|
|
57
57
|
// the manifest pins each skill's hash, so changing any file alone is detected.
|
|
58
|
-
const PINNED_PACK_SHA256 = "
|
|
58
|
+
const PINNED_PACK_SHA256 = "0460133ebbe3bc4f1ea03e297f36a15b72b291ab9b9efb80a35df863ef974e0c";
|
|
59
59
|
|
|
60
60
|
const PACK_DIR = new URL("../assets/skills/", import.meta.url);
|
|
61
61
|
const PACK_KEYS = ["commit", "manifest_version", "repository", "skills"];
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// Exact bytes of bundled skills from prior public releases and the previous bundle.
|
|
2
|
+
// A repository receipt cannot authorize replacing arbitrary user-authored bytes.
|
|
3
|
+
export const TRUSTED_SKILLS = {
|
|
4
|
+
"metergraph": [
|
|
5
|
+
"90f7d8d78a5b0b7a57436f194222f0c73310b0b04201c297c8fbf0b00ad6bb3f",
|
|
6
|
+
"d527a67efc42a55245b9c2c8fd2687c1f31f553e154a6242d0f31466993e314e",
|
|
7
|
+
"e7fc4eb7a22eeb16c357bede9d0a080ad3b415aaab1d9c72f164d5350d48c4b1",
|
|
8
|
+
"ebf784673eff0280d85974504f93c683a1dabfb54156c9d27e091131462e9d5f",
|
|
9
|
+
"482bdb6c32ad04766ba5159449a612419c90bfe78ea07038eb8d1ca7fb1edb76"
|
|
10
|
+
],
|
|
11
|
+
"metergraph-analyze": [
|
|
12
|
+
"c45756ad1d33a360c7c845ca299d8c1ab1e2b8162865f635bc25c78358c2a1e6",
|
|
13
|
+
"e8bf1debf62aa7d33d3dd363e5d95d8f781e8ca23cde4619b5bb4133dd47a7a5",
|
|
14
|
+
"4544f8183a1259ef805eb89a72346ecc87306c70007b235436d6425787b85474"
|
|
15
|
+
],
|
|
16
|
+
"metergraph-candidates": [
|
|
17
|
+
"acc0176e29ea5b867855c1f4691e01e4cb9295705b0ffc89ad0b348da9c47d8a",
|
|
18
|
+
"43de69d168091492310fc2f8851710e861a0b5cd3975c30a5c23d205abfcdfc6"
|
|
19
|
+
],
|
|
20
|
+
"metergraph-evals": [
|
|
21
|
+
"b115c53c5ff4040ac35ff240aee6cb4b56465db0916d9f104f8ab433bfc77c60",
|
|
22
|
+
"9baef426288455b2be2ea8af2774a19c344b5b83f8e9046916f24f8bf744f2de"
|
|
23
|
+
],
|
|
24
|
+
"metergraph-investigate": [
|
|
25
|
+
"470579faf9e8b5f1a562060d51ec5a71d1b9c47213403a2447dd40af56a74869",
|
|
26
|
+
"775dbe7ca1100b17638d0905302ef1324694a60b90372c6c3df7b598a8f70438",
|
|
27
|
+
"b06951926490aa0cd7a6ba0de5d8c1f21cac931ccab882552cea7ebea4ab7562"
|
|
28
|
+
],
|
|
29
|
+
"metergraph-iterate": [
|
|
30
|
+
"9ce5a2cf527e3f9a81fddcabc4bfc0a4e4894df7b502df8672e3d66470e5a5e8",
|
|
31
|
+
"1c2a2ad249b0fdf7f9f201d99ff76b91a7cdfbf57dd8f43d61612775b9982445"
|
|
32
|
+
],
|
|
33
|
+
"metergraph-local-bundle": [
|
|
34
|
+
"692cd8952a8a8896ec2c06091b329a7a01912e261e87d62a9458452172a7c9b0",
|
|
35
|
+
"c31286b7ee6dd8fc228e81565662ebaf4b0faf2ae92229602baf7c809202491b",
|
|
36
|
+
"3afd094368053023716c3c76893b51f6ae4b45e38211f69e2bbc5117c86d9a51"
|
|
37
|
+
],
|
|
38
|
+
"metergraph-model-swap": [
|
|
39
|
+
"d77ee37f882ab1e50dc4a093e64a2c3661f4cd9bb59f184da6dccbd76fd51a18",
|
|
40
|
+
"8fbf010d11b75bbe26fb13f776251038fab4688ccbfe58f3c582c469cf149433"
|
|
41
|
+
],
|
|
42
|
+
"metergraph-onboarding": [
|
|
43
|
+
"409d105f3b042514fd347a1fffc5489a94c01d1bc8aac55800e8c9f13a66a2c1",
|
|
44
|
+
"6e6f418933f37b34437035ab3171344d43f37dee3770815b19f97efb62e5f1d1",
|
|
45
|
+
"8ee73ecfe42894c74b16fbc3675a234452f152f248f8f574be3f04dd6e509805"
|
|
46
|
+
],
|
|
47
|
+
"metergraph-report": [
|
|
48
|
+
"0cdbeb684c4f7f87932784db832f376cff984fe0220a1167f1a73edd550450b7",
|
|
49
|
+
"58add8f6a32191e6fc01ca533a700e190a8e6da412e619bca38c65a481ea9fb5",
|
|
50
|
+
"90d52900067616330ce073d7d47b302120c4d0f4f8ffbef712eeec0a0bae4153"
|
|
51
|
+
],
|
|
52
|
+
"metergraph-workloads": [
|
|
53
|
+
"caf819780722f941b9d6b2f9564413a7deadda53439c973a7764be1b6eca98e5",
|
|
54
|
+
"bd28b1710fc5dfc082fa53bbcd32f8a3300338a37b82c14a87c8849d41a3fc24"
|
|
55
|
+
]
|
|
56
|
+
};
|
package/src/skill.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { TRUSTED_SKILLS } from "./skill-trusted.js";
|
|
2
|
+
|
|
1
3
|
import { randomBytes } from "node:crypto";
|
|
2
|
-
import fs from "
|
|
4
|
+
import fs, { validatePath, withRollback } from "./anchored-fs.js";
|
|
3
5
|
import path from "node:path";
|
|
4
6
|
|
|
5
7
|
import {
|
|
@@ -31,6 +33,7 @@ const ENTRY_KEYS = ["client", "path", "revision", "runtimes", "sha256", "skill"]
|
|
|
31
33
|
const NOFOLLOW = fs.constants.O_NOFOLLOW ?? 0;
|
|
32
34
|
|
|
33
35
|
const MESSAGES = {
|
|
36
|
+
secure_writes_unavailable: "Secure automatic file writes are unavailable on this platform or the bundled filesystem helper failed verification. Nothing was written.",
|
|
34
37
|
invalid_project: "--project must name an existing directory.",
|
|
35
38
|
client_not_supported:
|
|
36
39
|
"This client cannot load project skill files. Follow the connection guide instead. Nothing was written.",
|
|
@@ -114,7 +117,7 @@ function apply(root, context) {
|
|
|
114
117
|
return plan.status;
|
|
115
118
|
} finally {
|
|
116
119
|
try {
|
|
117
|
-
fs.unlinkSync(lockPath);
|
|
120
|
+
withRollback(() => fs.unlinkSync(lockPath));
|
|
118
121
|
} catch {
|
|
119
122
|
// A lock that cannot be removed is reported by the next run.
|
|
120
123
|
}
|
|
@@ -134,7 +137,8 @@ function resolveProject(project) {
|
|
|
134
137
|
try {
|
|
135
138
|
const real = fs.realpathSync(path.resolve(project ?? process.cwd()));
|
|
136
139
|
if (fs.statSync(real).isDirectory()) return real;
|
|
137
|
-
} catch {
|
|
140
|
+
} catch (error) {
|
|
141
|
+
if (error.code === "ENOTSUP") throw new Stop("unsupported", "secure_writes_unavailable");
|
|
138
142
|
// Fall through to the fixed error.
|
|
139
143
|
}
|
|
140
144
|
throw new Stop("invalid_input", "invalid_project");
|
|
@@ -180,6 +184,10 @@ function decide(action, runtime, state, bundle) {
|
|
|
180
184
|
if (file !== null && (file.content === null || sha256Hex(file.content) !== entry.sha256)) {
|
|
181
185
|
throw new Stop("conflict", "modified");
|
|
182
186
|
}
|
|
187
|
+
if (file !== null && entry.sha256 !== bundle.sha256 &&
|
|
188
|
+
!TRUSTED_SKILLS[bundle.name]?.includes(entry.sha256)) {
|
|
189
|
+
throw new Stop("conflict", "not_owned");
|
|
190
|
+
}
|
|
183
191
|
const current = entry.revision === bundle.revision;
|
|
184
192
|
if (!current && action === "install") throw new Stop("conflict", "update_required");
|
|
185
193
|
if (file !== null && current) {
|
|
@@ -200,6 +208,7 @@ function commit(root, context, state, plan) {
|
|
|
200
208
|
const receipt = nextReceipt(state.receipt, context, target.relative);
|
|
201
209
|
let createdDirs = [];
|
|
202
210
|
let skillWritten = false;
|
|
211
|
+
let receiptWritten = false;
|
|
203
212
|
try {
|
|
204
213
|
if (plan.writeSkill) {
|
|
205
214
|
createdDirs = ensureDirs(root, target.dirs);
|
|
@@ -208,13 +217,23 @@ function commit(root, context, state, plan) {
|
|
|
208
217
|
}
|
|
209
218
|
if (state.receiptFile === null || !state.receiptFile.content.equals(receipt)) {
|
|
210
219
|
replaceFile(receiptPath, receipt, state.receiptFile);
|
|
220
|
+
receiptWritten = true;
|
|
211
221
|
}
|
|
222
|
+
validatePath(root);
|
|
223
|
+
validatePath(path.dirname(skillPath));
|
|
224
|
+
validatePath(path.dirname(receiptPath));
|
|
212
225
|
} catch (error) {
|
|
213
226
|
try {
|
|
214
|
-
|
|
215
|
-
if (
|
|
216
|
-
|
|
217
|
-
|
|
227
|
+
withRollback(() => {
|
|
228
|
+
if (receiptWritten) {
|
|
229
|
+
if (state.receiptFile === null) fs.unlinkSync(receiptPath);
|
|
230
|
+
else replaceFile(receiptPath, state.receiptFile.content, { content: receipt, mode: state.receiptFile.mode });
|
|
231
|
+
}
|
|
232
|
+
if (skillWritten) {
|
|
233
|
+
if (state.file === null) fs.unlinkSync(skillPath);
|
|
234
|
+
else replaceFile(skillPath, state.file.content, { content: bundle.content, mode: state.file.mode });
|
|
235
|
+
}
|
|
236
|
+
});
|
|
218
237
|
} catch {
|
|
219
238
|
throw new Stop("filesystem_error", "rollback_failed");
|
|
220
239
|
}
|
|
@@ -325,7 +344,7 @@ function ensureDirs(root, dirs) {
|
|
|
325
344
|
function removeDirs(created) {
|
|
326
345
|
for (const dir of [...created].reverse()) {
|
|
327
346
|
try {
|
|
328
|
-
fs.rmdirSync(dir);
|
|
347
|
+
withRollback(() => fs.rmdirSync(dir));
|
|
329
348
|
} catch {
|
|
330
349
|
// Not empty or not removable. An empty directory is harmless.
|
|
331
350
|
}
|
|
@@ -382,7 +401,7 @@ function replaceFile(dest, content, previous) {
|
|
|
382
401
|
fs.renameSync(temp, dest);
|
|
383
402
|
} catch (error) {
|
|
384
403
|
try {
|
|
385
|
-
fs.unlinkSync(temp);
|
|
404
|
+
withRollback(() => fs.unlinkSync(temp));
|
|
386
405
|
} catch {
|
|
387
406
|
// Already renamed or never created.
|
|
388
407
|
}
|
package/src/style.js
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import { EXIT_CODES } from "./constants.js";
|
|
2
|
-
|
|
3
1
|
// Presentation for human (non --json) output: a status headline, aligned
|
|
4
2
|
// fields and next steps. Color is used only when the stream is a terminal
|
|
5
3
|
// and NO_COLOR is unset (https://no-color.org); FORCE_COLOR turns it on.
|
|
@@ -42,18 +40,38 @@ export function toneFor(outcome) {
|
|
|
42
40
|
return "fail";
|
|
43
41
|
}
|
|
44
42
|
|
|
45
|
-
export function headline(style, tone, title
|
|
43
|
+
export function headline(style, tone, title) {
|
|
46
44
|
const [icon, color] = TONES[tone];
|
|
47
|
-
|
|
48
|
-
|
|
45
|
+
const line = `${style[color](icon)} ${style.bold(title)}`;
|
|
46
|
+
// Machine outcomes and numeric codes belong to JSON and the scripting reference.
|
|
49
47
|
return line;
|
|
50
48
|
}
|
|
51
49
|
|
|
50
|
+
// How text output names a workspace: "Name (slug)", or the slug or name
|
|
51
|
+
// alone, from data the service verified. Never by its ID; null when neither
|
|
52
|
+
// is known, and the row is then left out.
|
|
53
|
+
export function workspaceShown(workspace) {
|
|
54
|
+
if (!workspace) return null;
|
|
55
|
+
const slug = workspace.slug ?? null;
|
|
56
|
+
const name = workspace.name ?? null;
|
|
57
|
+
if (slug !== null && name !== null) return `${name} (${slug})`;
|
|
58
|
+
return slug ?? name;
|
|
59
|
+
}
|
|
60
|
+
|
|
52
61
|
// Aligned "label value" rows. Rows whose value is null or undefined are left out.
|
|
53
62
|
export function fields(style, rows, indent = " ") {
|
|
54
63
|
const shown = rows.filter(([, value]) => value !== null && value !== undefined);
|
|
55
64
|
const width = Math.max(0, ...shown.map(([label]) => label.length));
|
|
56
|
-
return shown.
|
|
65
|
+
return shown.flatMap(([label, value]) => {
|
|
66
|
+
if (!Number.isFinite(style.width)) return [`${indent}${style.dim(label.padEnd(width))} ${value}`];
|
|
67
|
+
const prefix = `${indent}${label.padEnd(width)} `;
|
|
68
|
+
if (prefix.length > style.width / 2) {
|
|
69
|
+
return [`${indent}${style.dim(label)}`, ...paragraph(style, value, indent + " ")];
|
|
70
|
+
}
|
|
71
|
+
const wrapped = paragraph(style, value, " ".repeat(prefix.length));
|
|
72
|
+
if (!wrapped.length) return [`${indent}${style.dim(label.padEnd(width))} `];
|
|
73
|
+
return [`${indent}${style.dim(label.padEnd(width))} ${wrapped[0].trimStart()}`, ...wrapped.slice(1)];
|
|
74
|
+
});
|
|
57
75
|
}
|
|
58
76
|
|
|
59
77
|
// Indents prose and wraps it to style.width.
|
|
@@ -78,7 +96,15 @@ export function steps(style, rows) {
|
|
|
78
96
|
const shown = rows.filter(([, value]) => value !== null && value !== undefined);
|
|
79
97
|
const width = Math.max(0, ...shown.map(([label]) => label.length));
|
|
80
98
|
return shown.flatMap(([label, value]) => {
|
|
81
|
-
if (label)
|
|
99
|
+
if (label) {
|
|
100
|
+
if (!Number.isFinite(style.width)) return [`${style.cyan("→")} ${label.padEnd(width)} ${value}`];
|
|
101
|
+
const prefix = `→ ${label.padEnd(width)} `;
|
|
102
|
+
if (prefix.length > style.width / 2) {
|
|
103
|
+
return [`${style.cyan("→")} ${label}`, ...paragraph(style, value, " ")];
|
|
104
|
+
}
|
|
105
|
+
const wrapped = paragraph(style, value, " ".repeat(prefix.length));
|
|
106
|
+
return [`${style.cyan("→")} ${label.padEnd(width)} ${wrapped[0]?.trimStart() ?? ""}`, ...wrapped.slice(1)];
|
|
107
|
+
}
|
|
82
108
|
// Unlabeled steps are prose: wrap them with a hanging indent.
|
|
83
109
|
const [first, ...rest] = paragraph(style, value, " ");
|
|
84
110
|
return [`${style.cyan("→")} ${first.trimStart()}`, ...rest];
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { jsonText } from "./output.js";
|
|
2
|
+
import { noticeText, readMessage } from "./read-output.js";
|
|
3
|
+
import { PLAIN, fields, headline, paragraph, render, steps, toneFor, workspaceShown } from "./style.js";
|
|
4
|
+
|
|
5
|
+
// Fixed text for tools results. A tool's result and error are the service's
|
|
6
|
+
// own JSON, printed with jsonText so no control or bidirectional character
|
|
7
|
+
// reaches the terminal; tool titles are printed with them removed.
|
|
8
|
+
|
|
9
|
+
const MESSAGES = {
|
|
10
|
+
tool_unavailable:
|
|
11
|
+
"This credential cannot use that tool here: the name is unknown, the tool needs more access, or this " +
|
|
12
|
+
"deployment does not offer it. Nothing was called.",
|
|
13
|
+
confirmation_required:
|
|
14
|
+
"This tool writes, starts work or calls a model provider, so it runs only with --yes, once the person has " +
|
|
15
|
+
"approved this exact call. Nothing was sent.",
|
|
16
|
+
tool_error: "The service refused the call; the tool error below says why.",
|
|
17
|
+
tool_rejected: "The service rejected the call's arguments; the tool error below says why.",
|
|
18
|
+
tool_call_unconfirmed:
|
|
19
|
+
"The call reached the service, but its answer was not confirmed, so it may have taken effect. Check before " +
|
|
20
|
+
"calling it again.",
|
|
21
|
+
tool_response_invalid: "The service's answer to the tool request does not match the MCP protocol.",
|
|
22
|
+
tools_unavailable: "This service does not list Agent Access tools.",
|
|
23
|
+
args_invalid: "The arguments are not one JSON object. Nothing was sent.",
|
|
24
|
+
args_too_large: "The arguments are larger than 1 MiB. Nothing was sent.",
|
|
25
|
+
args_file_unreadable: "The --args-file file could not be read as a regular file. Nothing was sent.",
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export function toolsMessage(outcome, reason) {
|
|
29
|
+
return MESSAGES[reason] ?? readMessage(outcome, reason);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const CONTROL = /[\u061c\u0000-\u001f\u007f-\u009f--]/g;
|
|
33
|
+
const plain = (value) => (typeof value === "string" ? value.replace(CONTROL, "") : "");
|
|
34
|
+
|
|
35
|
+
function kindOf(tool) {
|
|
36
|
+
if (tool.read_only && !tool.open_world) return "read";
|
|
37
|
+
return tool.open_world ? "calls out, needs --yes" : "writes, needs --yes";
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function toolsText(result, message, style = PLAIN, action) {
|
|
41
|
+
const data = result.data;
|
|
42
|
+
const title = action === "list" ? "Agent Access tools" : `Tool ${data.result?.tool ?? ""}`.trim();
|
|
43
|
+
const lines = [headline(style, result.ok ? "info" : toneFor(result.outcome), result.ok ? title : result.outcome === "unconfirmed" ? "Tool call unconfirmed" : `${title} did not run`, result.outcome), ""];
|
|
44
|
+
lines.push(...fields(style, [["Origin", data.origin], ["Workspace", workspaceShown(data.workspace)], ["Credential", data.credential === "agent_key" ? "coding-agent key" : "saved sign in"]]), "");
|
|
45
|
+
for (const notice of data.notices) {
|
|
46
|
+
const message = noticeText(notice);
|
|
47
|
+
if (message) lines.push(...paragraph(style, message), "");
|
|
48
|
+
}
|
|
49
|
+
if (data.retry_after_seconds !== null) lines.push(...fields(style, [["Retry after", `${data.retry_after_seconds} seconds`]]), "");
|
|
50
|
+
if (result.ok && action === "list") {
|
|
51
|
+
const tools = data.result.tools;
|
|
52
|
+
const width = Math.max(4, ...tools.map((tool) => tool.name.length));
|
|
53
|
+
for (const tool of tools) {
|
|
54
|
+
lines.push(` ${tool.name.padEnd(width)} ${style.dim(kindOf(tool))}`);
|
|
55
|
+
if (tool.title) lines.push(` ${"".padEnd(width)} ${plain(tool.title)}`);
|
|
56
|
+
}
|
|
57
|
+
if (tools.length === 0) lines.push(" No tools are offered to this credential.");
|
|
58
|
+
lines.push("", ...steps(style, [["", "Call one with metergraph tools call NAME --args '{...}'. Add --json for its input schema."]]));
|
|
59
|
+
return render(lines);
|
|
60
|
+
}
|
|
61
|
+
if (result.ok) {
|
|
62
|
+
lines.push(...jsonText(data.result.result, 2).split("\n").map((line) => ` ${line}`));
|
|
63
|
+
return render(lines);
|
|
64
|
+
}
|
|
65
|
+
lines.push(...paragraph(style, message));
|
|
66
|
+
if (data.tool_error) {
|
|
67
|
+
lines.push("", ...jsonText(data.tool_error, 2).split("\n").map((line) => ` ${line}`));
|
|
68
|
+
}
|
|
69
|
+
if (data.next_action?.message) {
|
|
70
|
+
lines.push("", ...steps(style, [["", data.next_action.message], ["Run", data.next_action.command ?? null]]));
|
|
71
|
+
}
|
|
72
|
+
return render(lines);
|
|
73
|
+
}
|