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.
Files changed (59) hide show
  1. package/README.md +78 -14
  2. package/assets/skill/SKILL.md +124 -31
  3. package/assets/skill/manifest.json +3 -3
  4. package/assets/skills/manifest.json +31 -31
  5. package/assets/skills/metergraph-analyze/SKILL.md +161 -58
  6. package/assets/skills/metergraph-candidates/SKILL.md +50 -8
  7. package/assets/skills/metergraph-evals/SKILL.md +83 -16
  8. package/assets/skills/metergraph-investigate/SKILL.md +90 -34
  9. package/assets/skills/metergraph-iterate/SKILL.md +82 -12
  10. package/assets/skills/metergraph-local-bundle/SKILL.md +146 -128
  11. package/assets/skills/metergraph-model-swap/SKILL.md +142 -20
  12. package/assets/skills/metergraph-onboarding/SKILL.md +93 -49
  13. package/assets/skills/metergraph-report/SKILL.md +60 -15
  14. package/assets/skills/metergraph-workloads/SKILL.md +97 -13
  15. package/native/README.md +85 -0
  16. package/native/anchored-fs.c +243 -0
  17. package/native/bin/anchored-fs-darwin-arm64 +0 -0
  18. package/native/bin/anchored-fs-darwin-x64 +0 -0
  19. package/native/bin/anchored-fs-linux-arm64 +0 -0
  20. package/native/bin/anchored-fs-linux-x64 +0 -0
  21. package/native/build.mjs +55 -0
  22. package/native/manifest.json +9 -0
  23. package/native/targets.mjs +24 -0
  24. package/package.json +10 -3
  25. package/src/action-help.js +130 -0
  26. package/src/agent-key.js +21 -14
  27. package/src/anchored-fs.js +163 -0
  28. package/src/args.js +125 -17
  29. package/src/auth-binding.js +12 -7
  30. package/src/auth-callback.js +10 -2
  31. package/src/auth-login.js +111 -28
  32. package/src/auth-oauth.js +89 -45
  33. package/src/auth-session.js +19 -12
  34. package/src/auth-store.js +28 -7
  35. package/src/cli.js +52 -8
  36. package/src/constants.js +37 -9
  37. package/src/experiments-output.js +7 -6
  38. package/src/experiments.js +26 -17
  39. package/src/output.js +274 -79
  40. package/src/read-contract.js +42 -8
  41. package/src/read-output.js +66 -13
  42. package/src/read.js +90 -15
  43. package/src/repository-identity.js +2 -2
  44. package/src/setup-env-git.js +1 -1
  45. package/src/setup-env.js +16 -9
  46. package/src/setup-interactive.js +19 -4
  47. package/src/setup-refusal.js +2 -2
  48. package/src/setup-state.js +9 -4
  49. package/src/setup.js +15 -3
  50. package/src/skill-bundle.js +2 -2
  51. package/src/skill-trusted.js +56 -0
  52. package/src/skill.js +28 -9
  53. package/src/style.js +33 -7
  54. package/src/tools-output.js +73 -0
  55. package/src/tools.js +249 -0
  56. package/src/trace-contract.js +1 -1
  57. package/src/transport.js +21 -0
  58. package/src/verify-output.js +2 -2
  59. package/src/verify.js +5 -2
@@ -1,5 +1,5 @@
1
1
  import { randomBytes, randomUUID } from "node:crypto";
2
- import fs from "node:fs";
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
  }
@@ -1,7 +1,9 @@
1
1
  import { randomBytes, randomUUID } from "node:crypto";
2
- import fs from "node:fs";
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
- if (!saved) {
116
- const early = currentEnvValues(preflightEnv({ project: root, envFile: options.envFile, signal: trap.signal }));
117
- if (early.token !== null) return stop("conflict", "existing_ingest_key_unowned");
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`) {
@@ -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 = "ebf784673eff0280d85974504f93c683a1dabfb54156c9d27e091131462e9d5f";
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 = "11ab983875911c4c2ad03d2a3b7b9e9213c410e77f4ff2d27b4059354b00fe0c";
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 "node:fs";
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
- if (skillWritten) {
215
- if (state.file === null) fs.unlinkSync(skillPath);
216
- else replaceFile(skillPath, state.file.content, { content: bundle.content, mode: state.file.mode });
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, outcome = null) {
43
+ export function headline(style, tone, title) {
46
44
  const [icon, color] = TONES[tone];
47
- let line = `${style[color](icon)} ${style.bold(title)}`;
48
- if (outcome !== null && outcome !== "ok") line += ` ${style.dim(`(${outcome}, exit ${EXIT_CODES[outcome]})`)}`;
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.map(([label, value]) => `${indent}${style.dim(label.padEnd(width))} ${value}`);
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) return [`${style.cyan("→")} ${label.padEnd(width)} ${value}`];
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
+ }