@knowttl/mg-axi 0.1.8 → 0.1.9

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/dist/catalogue.js CHANGED
@@ -2875,9 +2875,10 @@ export function operationFor(leaf, version) {
2875
2875
  const inventory = JSON.parse(readFileSync(new URL("../inventory/operations.json", import.meta.url), "utf8"));
2876
2876
  return inventory.operations.find((row) => row.id === `${version}:${leaf.operation}`);
2877
2877
  }
2878
- // Next-step hints shared by the no-args home view and the installable
2878
+ // Base next-step hints shared by the no-args home view and the installable
2879
2879
  // skill, so the two cannot drift apart. home() prints them alongside live
2880
- // profile status; skillDocument() in docs.ts reprints them.
2880
+ // profile status; skillDocument() in docs.ts reprints them. localHome() in
2881
+ // cli.ts may append one conditional skill install hint (see skill-hint.ts).
2881
2882
  export const HOME_HELP = ["npx -y @knowttl/mg-axi setup", "npx -y @knowttl/mg-axi doctor", "npx -y @knowttl/mg-axi entra user list --help", "npx -y @knowttl/mg-axi entra user show --help", "npx -y @knowttl/mg-axi entra group list --help", "npx -y @knowttl/mg-axi entra group member list --help", "npx -y @knowttl/mg-axi entra application list --help", "npx -y @knowttl/mg-axi entra service-principal list --help", "npx -y @knowttl/mg-axi entra conditional-access policy list --help", "npx -y @knowttl/mg-axi api get --help"];
2882
2883
  // A useful local status, without treating unavailable tenant summaries as zeros.
2883
2884
  export function home() {
package/dist/cli.js CHANGED
@@ -6,6 +6,7 @@ import { AxiError, installSessionStartHooks, runAxiCli } from "axi-sdk-js";
6
6
  import { LEAVES, home, leafHelp, operationFor, resolveCommand, DESCRIPTION, TOP_LEVEL_HELP } from "./catalogue.js";
7
7
  import { VERSION } from "./version.js";
8
8
  import { Profiles } from "./profiles.js";
9
+ import { skillHint } from "./skill-hint.js";
9
10
  import { authStatus } from "./auth-status.js";
10
11
  import { redactSecrets } from "./redact.js";
11
12
  import { GraphSession, MAX_CURSOR_BYTES } from "./graph-session.js";
@@ -66,6 +67,10 @@ function localHome(name, config) {
66
67
  output.profile = "unavailable: no default profile selected";
67
68
  }
68
69
  output.help.unshift("npx -y @knowttl/mg-axi profile list", "npx -y @knowttl/mg-axi auth login --help");
70
+ // Point agents at the installable skill until it is installed for their host.
71
+ const hint = skillHint();
72
+ if (hint !== undefined)
73
+ output.help.push(hint);
69
74
  return output;
70
75
  }
71
76
  // One dispatch for the hook installer: resolve the sibling hook entry point
package/dist/docs.js CHANGED
@@ -135,9 +135,11 @@ export function skillDescription() {
135
135
  const writes = skillWriteLabels();
136
136
  return `Inspect Microsoft Entra ${areas.join(", ")} and run ${writes.length} gated writes (${writes.join(", ")}) through token-efficient TOON output.`;
137
137
  }
138
- // Next-step hints printed by the no-args home view. The two leading entries mirror the
138
+ // Base next-step hints printed by the no-args home view. The two leading entries mirror the
139
139
  // profile/login hints localHome() in cli.ts prepends to HOME_HELP; the
140
140
  // rest is HOME_HELP verbatim, so skill and home view cannot drift apart.
141
+ // localHome() may append one conditional skill install hint from skill-hint.ts
142
+ // when the skill is missing for the current host; the installed skill omits it.
141
143
  export function skillHomeHints() {
142
144
  return ["npx -y @knowttl/mg-axi profile list", "npx -y @knowttl/mg-axi auth login --help", ...HOME_HELP];
143
145
  }
@@ -0,0 +1,138 @@
1
+ // One-line skill install hint for the no-args home view. Detects the agent
2
+ // host from its documented environment marker and checks whether the mg-axi
3
+ // skill is already installed for that host, at user, project (working
4
+ // directory upwards) and system level. File-existence checks only: this never
5
+ // installs, writes, fails or touches the network.
6
+ //
7
+ // The check looks only at the detected host's row, so one host's copy never
8
+ // hides another host's hint. Each row lists every documented skill folder
9
+ // that host loads, verified against the real hosts:
10
+ // - Claude Code reads its config directory ($CLAUDE_CONFIG_DIR or ~/.claude,
11
+ // per its binary) plus ~/.agents/skills and .agents/skills (per its
12
+ // binary); project .claude/skills loads from the working directory up to
13
+ // the repository root (per https://code.claude.com/docs/en/skills). When
14
+ // the working directory is inside a linked git worktree whose root has no
15
+ // .claude/skills directory, Claude Code (v2.1.277+) falls back to the main
16
+ // checkout's project skills (per https://code.claude.com/docs/en/worktrees).
17
+ // - Codex reads $HOME/.agents/skills and .agents/skills (cwd to repo root)
18
+ // per https://learn.chatgpt.com/docs/build-skills.md, plus
19
+ // $CODEX_HOME/skills (default ~/.codex/skills, per its binary and the
20
+ // skills-CLI registry) and the documented /etc/codex/skills admin folder.
21
+ // - Pi reads its user/project skills directories plus the Agent Skills
22
+ // locations ~/.agents/skills/ and .agents/skills/ (ancestors to the repo
23
+ // root) per its skills.md.
24
+ // - OpenCode reads its config directory ($OPENCODE_CONFIG_DIR,
25
+ // $XDG_CONFIG_HOME or ~/.config, per its binary and CLI docs) plus
26
+ // ~/.claude/skills, ~/.agents/skills, .opencode/skills, .claude/skills and
27
+ // .agents/skills (project paths walk up to the git worktree) per
28
+ // https://opencode.ai/docs/skills/.
29
+ //
30
+ // Only a direct-child <folder>/mg-axi/SKILL.md counts as installed. Folders
31
+ // listed in host settings files (for example Pi settings.json) and recursive
32
+ // nested layouts are a documented limitation and are deliberately not
33
+ // implemented.
34
+ import { existsSync, readFileSync } from "node:fs";
35
+ import { homedir } from "node:os";
36
+ import { dirname, join, resolve } from "node:path";
37
+ export const SKILL_NAME = "mg-axi";
38
+ export const SKILL_INSTALL_COMMAND = `npx skills add knowttl/${SKILL_NAME} --skill ${SKILL_NAME}`;
39
+ function skillFile(dir) {
40
+ return join(dir, SKILL_NAME, "SKILL.md");
41
+ }
42
+ function resolveHome(env) {
43
+ return env.HOME ?? env.USERPROFILE ?? homedir();
44
+ }
45
+ export function skillHostRows(env, home) {
46
+ const agentsUser = join(home, ".agents", "skills");
47
+ const agentsProject = join(".agents", "skills");
48
+ return [
49
+ {
50
+ name: "claude-code",
51
+ userDirs: [join(env.CLAUDE_CONFIG_DIR ?? join(home, ".claude"), "skills"), agentsUser],
52
+ projectDirs: [join(".claude", "skills"), agentsProject],
53
+ systemDirs: [],
54
+ },
55
+ {
56
+ name: "codex",
57
+ userDirs: [join(env.CODEX_HOME ?? join(home, ".codex"), "skills"), agentsUser],
58
+ projectDirs: [agentsProject],
59
+ systemDirs: ["/etc/codex/skills"],
60
+ },
61
+ {
62
+ name: "pi",
63
+ userDirs: [join(home, ".pi", "agent", "skills"), agentsUser],
64
+ projectDirs: [join(".pi", "skills"), agentsProject],
65
+ systemDirs: [],
66
+ },
67
+ {
68
+ name: "opencode",
69
+ userDirs: [
70
+ join(env.OPENCODE_CONFIG_DIR ?? env.XDG_CONFIG_HOME ?? join(home, ".config"), "opencode", "skills"),
71
+ join(home, ".claude", "skills"),
72
+ agentsUser,
73
+ ],
74
+ projectDirs: [join(".opencode", "skills"), join(".claude", "skills"), agentsProject],
75
+ systemDirs: [],
76
+ },
77
+ ];
78
+ }
79
+ // Marker variables each host sets for its own child processes, verified
80
+ // against the real hosts: CLAUDECODE (Claude Code docs), CODEX_THREAD_ID /
81
+ // CODEX_SESSION_ID (codex shell_environment.rs), PI_CODING_AGENT (pi docs),
82
+ // OPENCODE (opencode startup middleware, inherited by its shell children).
83
+ export function detectSkillHost(env, home) {
84
+ const rows = skillHostRows(env, home);
85
+ if (env.CLAUDECODE !== undefined)
86
+ return rows.find(row => row.name === "claude-code");
87
+ if (env.CODEX_THREAD_ID !== undefined || env.CODEX_SESSION_ID !== undefined)
88
+ return rows.find(row => row.name === "codex");
89
+ if (env.PI_CODING_AGENT !== undefined)
90
+ return rows.find(row => row.name === "pi");
91
+ if (env.OPENCODE !== undefined)
92
+ return rows.find(row => row.name === "opencode");
93
+ return undefined;
94
+ }
95
+ // Main-checkout fallback for a linked git worktree: <worktree>/.git is a file
96
+ // reading "gitdir: <main>/.git/worktrees/<name>". Applies only when the
97
+ // worktree root has no .claude/skills directory of its own.
98
+ function claudeWorktreeSkill(fromDir) {
99
+ for (let dir = resolve(fromDir);; dir = dirname(dir)) {
100
+ let gitdir;
101
+ try {
102
+ const text = readFileSync(join(dir, ".git"), "utf8");
103
+ const prefix = "gitdir: ";
104
+ if (text.startsWith(prefix))
105
+ gitdir = resolve(dir, text.slice(prefix.length).trim());
106
+ }
107
+ catch {
108
+ gitdir = undefined;
109
+ }
110
+ if (gitdir !== undefined) {
111
+ const splitAt = gitdir.replace(/\\/g, "/").indexOf("/.git/worktrees/");
112
+ if (splitAt === -1 || existsSync(join(dir, ".claude", "skills")))
113
+ return false;
114
+ return existsSync(skillFile(join(gitdir.slice(0, splitAt), ".claude", "skills")));
115
+ }
116
+ if (existsSync(join(dir, ".git")) || dirname(dir) === dir)
117
+ return false;
118
+ }
119
+ }
120
+ export function skillInstalled(row, fromDir) {
121
+ if ([...row.userDirs, ...row.systemDirs].some(dir => existsSync(skillFile(dir))))
122
+ return true;
123
+ for (let dir = resolve(fromDir);; dir = dirname(dir)) {
124
+ if (row.projectDirs.some(projectDir => existsSync(skillFile(join(dir, projectDir)))))
125
+ return true;
126
+ if (dirname(dir) === dir)
127
+ break;
128
+ }
129
+ if (row.name === "claude-code")
130
+ return claudeWorktreeSkill(fromDir);
131
+ return false;
132
+ }
133
+ export function skillHint(env = process.env, fromDir = process.cwd()) {
134
+ const host = detectSkillHost(env, resolveHome(env));
135
+ if (host === undefined || skillInstalled(host, fromDir))
136
+ return undefined;
137
+ return SKILL_INSTALL_COMMAND;
138
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@knowttl/mg-axi",
3
- "version": "0.1.8",
3
+ "version": "0.1.9",
4
4
  "description": "Agent-facing Microsoft Graph CLI, read-only by default",
5
5
  "type": "module",
6
6
  "bin": {