argocd-axi 0.1.6 → 0.1.7
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 +16 -9
- package/dist/src/cli.js +2 -0
- package/dist/src/commands/context.js +83 -0
- package/dist/src/commands/setup.js +26 -3
- package/dist/src/help.js +3 -1
- package/dist/src/session-hook.js +117 -0
- package/dist/src/sessionlog.js +347 -0
- package/dist/src/version.js +1 -1
- package/package.json +1 -1
- package/skills/argocd-axi/SKILL.md +4 -2
package/README.md
CHANGED
|
@@ -121,14 +121,20 @@ and every other command that prints or changes a credential.
|
|
|
121
121
|
|
|
122
122
|
Pick one; you do not need both.
|
|
123
123
|
|
|
124
|
-
- **Session hook (recommended).** `argocd-axi setup hooks` installs
|
|
125
|
-
SessionStart
|
|
126
|
-
`argocd-axi-ctx`, which is local only:
|
|
127
|
-
and the cached server version. It makes
|
|
128
|
-
refresh and reads no credential, so a dead
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
124
|
+
- **Session hook (recommended).** `argocd-axi setup hooks` installs
|
|
125
|
+
SessionStart hooks for Claude Code, Codex and OpenCode, plus a Claude Code
|
|
126
|
+
SessionEnd hook. The start hook runs `argocd-axi-ctx`, which is local only:
|
|
127
|
+
current context, server, CLI version and the cached server version. It makes
|
|
128
|
+
no network call, triggers no SSO refresh and reads no credential, so a dead
|
|
129
|
+
context can never hang a session start. The end hook records only argocd-axi
|
|
130
|
+
command names, their counts and the most-used `--context` value for that
|
|
131
|
+
working directory; it does not save arguments or other values. The session
|
|
132
|
+
log is limited to 100 directories and stored with `0600` permissions. Run
|
|
133
|
+
`argocd-axi context` to view the current directory's last session, or
|
|
134
|
+
`argocd-axi setup status` / `argocd-axi setup uninstall` to inspect or remove
|
|
135
|
+
the Claude Code SessionEnd hook. Hooks need a persistent install
|
|
136
|
+
(`npm install -g argocd-axi`); run from an npx cache, installation is refused
|
|
137
|
+
because hooks would point at a temporary path.
|
|
132
138
|
- **Skill.** `npx skills add Dematom1/argocd-axi --skill argocd-axi`
|
|
133
139
|
installs [`skills/argocd-axi/SKILL.md`](skills/argocd-axi/SKILL.md), which
|
|
134
140
|
is generated from the CLI's own help and loads when a task is about Argo CD.
|
|
@@ -156,7 +162,8 @@ argocd-axi apps actions run <app> <action> --kind <k> --name <resource> [--confi
|
|
|
156
162
|
argocd-axi projects | projects view <p> | projects windows <p>
|
|
157
163
|
argocd-axi appsets | appsets view <name> | appsets generate <file>
|
|
158
164
|
argocd-axi repos | clusters
|
|
159
|
-
argocd-axi
|
|
165
|
+
argocd-axi context [end]
|
|
166
|
+
argocd-axi setup hooks | status | uninstall
|
|
160
167
|
argocd-axi update [--check]
|
|
161
168
|
```
|
|
162
169
|
|
package/dist/src/cli.js
CHANGED
|
@@ -10,6 +10,7 @@ import { homeCommand } from "./commands/home.js";
|
|
|
10
10
|
import { clustersCommand, reposCommand } from "./commands/inventory.js";
|
|
11
11
|
import { projectsCommand } from "./commands/projects.js";
|
|
12
12
|
import { setupCommand } from "./commands/setup.js";
|
|
13
|
+
import { contextCommand } from "./commands/context.js";
|
|
13
14
|
import { DESCRIPTION, TOP_HELP } from "./help.js";
|
|
14
15
|
import { NATIVE_NOUNS, REFUSALS } from "./registry.js";
|
|
15
16
|
import { renderBlocks, renderHelp } from "./toon.js";
|
|
@@ -55,6 +56,7 @@ export const COMMANDS = Object.assign(Object.create(null), {
|
|
|
55
56
|
repos: reposCommand,
|
|
56
57
|
clusters: clustersCommand,
|
|
57
58
|
setup: setupCommand,
|
|
59
|
+
context: contextCommand,
|
|
58
60
|
...Object.fromEntries(TOP_LEVEL_REFUSALS.map((key) => [key, refusalCommand(key)])),
|
|
59
61
|
});
|
|
60
62
|
export async function main(argv = process.argv.slice(2)) {
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { realpathSync } from "node:fs";
|
|
2
|
+
import { parseArgs } from "../args.js";
|
|
3
|
+
import { CODES, axiError } from "../errors.js";
|
|
4
|
+
import { encodeObject } from "../toon.js";
|
|
5
|
+
import { finish } from "./common.js";
|
|
6
|
+
import { lastSession, recordSession, sessionText, summarizeTranscript, } from "../sessionlog.js";
|
|
7
|
+
function isRecord(value) {
|
|
8
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
9
|
+
}
|
|
10
|
+
function directory(path) {
|
|
11
|
+
try {
|
|
12
|
+
return realpathSync(path);
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
return path;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
function display(summary) {
|
|
19
|
+
return summary ? sessionText(summary) : undefined;
|
|
20
|
+
}
|
|
21
|
+
const CONTEXT_SPEC = {
|
|
22
|
+
path: "context",
|
|
23
|
+
summary: "Show or record session memory",
|
|
24
|
+
positionals: [{ name: "action", required: false, description: "end" }],
|
|
25
|
+
flags: [],
|
|
26
|
+
examples: ["argocd-axi context", "argocd-axi context end"],
|
|
27
|
+
};
|
|
28
|
+
export async function contextCommand(args) {
|
|
29
|
+
const parsed = parseArgs(CONTEXT_SPEC, args);
|
|
30
|
+
const sub = parsed.positionals[0];
|
|
31
|
+
if (sub !== undefined && sub !== "end") {
|
|
32
|
+
throw axiError("unknown context action", CODES.VALIDATION_ERROR, [
|
|
33
|
+
"Run `argocd-axi context` or `argocd-axi context end`",
|
|
34
|
+
]);
|
|
35
|
+
}
|
|
36
|
+
if (sub !== "end") {
|
|
37
|
+
const summary = display(lastSession(directory(process.cwd())));
|
|
38
|
+
return finish([encodeObject({ last_session: summary })], undefined);
|
|
39
|
+
}
|
|
40
|
+
if (process.stdin.isTTY)
|
|
41
|
+
throw axiError("context end reads a SessionEnd hook payload on stdin", CODES.VALIDATION_ERROR, ["It runs from the hook installed by `argocd-axi setup hooks`"]);
|
|
42
|
+
let payload;
|
|
43
|
+
try {
|
|
44
|
+
payload = JSON.parse(await new Promise((resolve, reject) => {
|
|
45
|
+
let text = "";
|
|
46
|
+
process.stdin.setEncoding("utf8");
|
|
47
|
+
process.stdin.on("data", (chunk) => (text += chunk));
|
|
48
|
+
process.stdin.on("end", () => resolve(text));
|
|
49
|
+
process.stdin.on("error", reject);
|
|
50
|
+
}));
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return finish([encodeObject({ recorded: "nothing - stdin is not a hook payload" })], undefined);
|
|
54
|
+
}
|
|
55
|
+
if (!isRecord(payload) || typeof payload.transcript_path !== "string")
|
|
56
|
+
return finish([encodeObject({ recorded: "nothing - payload has no transcript_path" })], undefined);
|
|
57
|
+
let summary;
|
|
58
|
+
try {
|
|
59
|
+
summary = await summarizeTranscript(payload.transcript_path);
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return finish([encodeObject({ recorded: "nothing - transcript could not be read" })], undefined);
|
|
63
|
+
}
|
|
64
|
+
if (!summary)
|
|
65
|
+
return finish([
|
|
66
|
+
encodeObject({
|
|
67
|
+
recorded: "nothing - no argocd-axi commands this session",
|
|
68
|
+
}),
|
|
69
|
+
], undefined);
|
|
70
|
+
const cwd = typeof payload.cwd === "string" ? payload.cwd : process.cwd();
|
|
71
|
+
try {
|
|
72
|
+
if (!(await recordSession(directory(cwd), summary)))
|
|
73
|
+
return finish([encodeObject({ recorded: "nothing - session log is busy" })], undefined);
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
return finish([
|
|
77
|
+
encodeObject({
|
|
78
|
+
recorded: "nothing - session log could not be written",
|
|
79
|
+
}),
|
|
80
|
+
], undefined);
|
|
81
|
+
}
|
|
82
|
+
return finish([encodeObject({ recorded: sessionText(summary) })], undefined);
|
|
83
|
+
}
|
|
@@ -6,14 +6,16 @@ import { parseArgs, renderCommandHelp } from "../args.js";
|
|
|
6
6
|
import { CODES, axiError, validationError } from "../errors.js";
|
|
7
7
|
import { encodeObject } from "../toon.js";
|
|
8
8
|
import { finish } from "./common.js";
|
|
9
|
+
import { installSessionEndHook, sessionEndHookStatus, uninstallSessionEndHook, } from "../session-hook.js";
|
|
9
10
|
export const SETUP_HOOKS_SPEC = {
|
|
10
11
|
path: "setup hooks",
|
|
11
|
-
summary: "Install or
|
|
12
|
+
summary: "Install, inspect or remove agent session hooks",
|
|
12
13
|
positionals: [],
|
|
13
14
|
flags: [],
|
|
14
15
|
examples: ["argocd-axi setup hooks"],
|
|
15
16
|
notes: [
|
|
16
17
|
"The hook runs `argocd-axi-ctx`, which is local only: current context, server and CLI version, with no network call, no SSO refresh and no credential read",
|
|
18
|
+
"SessionEnd records command names, counts and the most-used --context value only; setup status and setup uninstall inspect or remove that Claude Code hook",
|
|
17
19
|
"Intended for global installs; repeated runs are no-ops",
|
|
18
20
|
],
|
|
19
21
|
};
|
|
@@ -29,16 +31,31 @@ function ctxEntrypoint() {
|
|
|
29
31
|
: join(here, "..", "..", "bin", "argocd-axi-ctx.ts");
|
|
30
32
|
}
|
|
31
33
|
export async function setupCommand(args) {
|
|
32
|
-
|
|
34
|
+
const action = args[0];
|
|
35
|
+
if (action !== "hooks" && action !== "status" && action !== "uninstall") {
|
|
33
36
|
if (args.includes("--help"))
|
|
34
37
|
return renderCommandHelp(SETUP_HOOKS_SPEC);
|
|
35
38
|
throw validationError("unknown setup action", [
|
|
36
|
-
"Run `argocd-axi setup hooks`",
|
|
39
|
+
"Run `argocd-axi setup hooks`, `argocd-axi setup status` or `argocd-axi setup uninstall`",
|
|
37
40
|
]);
|
|
38
41
|
}
|
|
39
42
|
if (args.includes("--help"))
|
|
40
43
|
return renderCommandHelp(SETUP_HOOKS_SPEC);
|
|
41
44
|
parseArgs(SETUP_HOOKS_SPEC, args.slice(1));
|
|
45
|
+
if (action === "status")
|
|
46
|
+
return finish([
|
|
47
|
+
encodeObject({
|
|
48
|
+
sessionEnd: sessionEndHookStatus() ? "installed" : "not installed",
|
|
49
|
+
}),
|
|
50
|
+
], undefined);
|
|
51
|
+
if (action === "uninstall")
|
|
52
|
+
return finish([
|
|
53
|
+
encodeObject({
|
|
54
|
+
sessionEnd: uninstallSessionEndHook()
|
|
55
|
+
? "removed"
|
|
56
|
+
: "not installed (no-op)",
|
|
57
|
+
}),
|
|
58
|
+
], undefined);
|
|
42
59
|
// An npx (or pnpm dlx, bunx) run lives in a cache that later sessions may
|
|
43
60
|
// not have on PATH or may have evicted, so a hook pointing at it would
|
|
44
61
|
// silently stop working. Require a persistent install instead.
|
|
@@ -56,6 +73,12 @@ export async function setupCommand(args) {
|
|
|
56
73
|
distEntrypoints: ["dist/bin/argocd-axi-ctx.js"],
|
|
57
74
|
onError: (message) => errors.push(message),
|
|
58
75
|
});
|
|
76
|
+
try {
|
|
77
|
+
installSessionEndHook();
|
|
78
|
+
}
|
|
79
|
+
catch (error) {
|
|
80
|
+
errors.push(error instanceof Error ? error.message : String(error));
|
|
81
|
+
}
|
|
59
82
|
if (errors.length > 0) {
|
|
60
83
|
throw axiError(`hook installation reported problems: ${errors.join("; ")}`, CODES.UPSTREAM_ERROR, ["Re-run `argocd-axi setup hooks` after fixing the paths above"]);
|
|
61
84
|
}
|
package/dist/src/help.js
CHANGED
|
@@ -4,6 +4,8 @@ export const DESCRIPTION = "Argo CD for agents: read operations plus opt-in writ
|
|
|
4
4
|
export const COMMAND_LINES = [
|
|
5
5
|
"(no command): live overview - context, server, user, apps needing attention with the reason, running operations",
|
|
6
6
|
"ctx [--context <c>]: current context, server and CLI version, local only (what the session hook prints)",
|
|
7
|
+
"context: last session's argocd-axi command summary for this directory",
|
|
8
|
+
"context end: record the current Claude Code session summary (SessionEnd hook)",
|
|
7
9
|
"ctx use <context>: switch the argocd CLI's current context (local config only)",
|
|
8
10
|
"auth guide [--context <c>]: offline human login and agent token setup instructions",
|
|
9
11
|
"whoami: user, issuer and groups of the current credential",
|
|
@@ -26,7 +28,7 @@ export const COMMAND_LINES = [
|
|
|
26
28
|
"projects | projects view <p> | projects windows <p>: projects, their rules, and whether sync is allowed now",
|
|
27
29
|
"appsets | appsets view <name> | appsets generate <file>: ApplicationSets and what a spec would generate",
|
|
28
30
|
"repos | clusters: repositories and destination clusters with connection status",
|
|
29
|
-
"setup hooks:
|
|
31
|
+
"setup hooks | status | uninstall: manage the Claude Code SessionEnd memory hook; setup hooks also installs SessionStart hooks",
|
|
30
32
|
];
|
|
31
33
|
export const TOP_HELP = `${renderBlocks([
|
|
32
34
|
"usage: argocd-axi <command> [args] [flags]",
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, statSync, unlinkSync, writeFileSync, } from "node:fs";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
const path = () => join(process.env.CLAUDE_CONFIG_DIR || join(homedir(), ".claude"), "settings.json");
|
|
6
|
+
const command = "argocd-axi context end";
|
|
7
|
+
function isRecord(value) {
|
|
8
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
9
|
+
}
|
|
10
|
+
function settings() {
|
|
11
|
+
const file = path();
|
|
12
|
+
if (!existsSync(file))
|
|
13
|
+
return {};
|
|
14
|
+
const value = JSON.parse(readFileSync(file, "utf8"));
|
|
15
|
+
if (!isRecord(value))
|
|
16
|
+
throw new Error(`${file} is not a JSON object`);
|
|
17
|
+
return value;
|
|
18
|
+
}
|
|
19
|
+
function writeSettings(value) {
|
|
20
|
+
const file = path();
|
|
21
|
+
mkdirSync(join(file, ".."), { recursive: true });
|
|
22
|
+
const mode = existsSync(file) ? statSync(file).mode & 0o777 : 0o600;
|
|
23
|
+
const temporary = `${file}.${randomUUID()}.tmp`;
|
|
24
|
+
try {
|
|
25
|
+
writeFileSync(temporary, `${JSON.stringify(value, null, 2)}\n`, { mode });
|
|
26
|
+
chmodSync(temporary, mode);
|
|
27
|
+
renameSync(temporary, file);
|
|
28
|
+
}
|
|
29
|
+
catch (error) {
|
|
30
|
+
try {
|
|
31
|
+
unlinkSync(temporary);
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
/* No temporary file to remove. */
|
|
35
|
+
}
|
|
36
|
+
throw error;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
export function installSessionEndHook() {
|
|
40
|
+
const value = settings();
|
|
41
|
+
const hooks = isRecord(value.hooks) ? value.hooks : {};
|
|
42
|
+
value.hooks = hooks;
|
|
43
|
+
const groups = Array.isArray(hooks.SessionEnd) ? hooks.SessionEnd : [];
|
|
44
|
+
const next = [];
|
|
45
|
+
let installed = false;
|
|
46
|
+
for (const group of groups) {
|
|
47
|
+
if (!isRecord(group) || !Array.isArray(group.hooks)) {
|
|
48
|
+
next.push(group);
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
const kept = group.hooks.filter((entry) => {
|
|
52
|
+
const ours = isRecord(entry) &&
|
|
53
|
+
(entry.command === command ||
|
|
54
|
+
(typeof entry.command === "string" &&
|
|
55
|
+
entry.command.endsWith("/argocd-axi context end")));
|
|
56
|
+
if (ours && !installed) {
|
|
57
|
+
installed = true;
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
return !ours;
|
|
61
|
+
});
|
|
62
|
+
if (kept.length)
|
|
63
|
+
next.push({ ...group, hooks: kept });
|
|
64
|
+
}
|
|
65
|
+
next.push({
|
|
66
|
+
matcher: "",
|
|
67
|
+
hooks: [{ type: "command", command, timeout: 30 }],
|
|
68
|
+
});
|
|
69
|
+
hooks.SessionEnd = next;
|
|
70
|
+
writeSettings(value);
|
|
71
|
+
}
|
|
72
|
+
export function sessionEndHookStatus() {
|
|
73
|
+
try {
|
|
74
|
+
const hooks = settings().hooks;
|
|
75
|
+
const groups = isRecord(hooks) ? hooks.SessionEnd : undefined;
|
|
76
|
+
return (Array.isArray(groups) &&
|
|
77
|
+
groups.some((group) => isRecord(group) &&
|
|
78
|
+
Array.isArray(group.hooks) &&
|
|
79
|
+
group.hooks.some((entry) => isRecord(entry) &&
|
|
80
|
+
(entry.command === command ||
|
|
81
|
+
(typeof entry.command === "string" &&
|
|
82
|
+
entry.command.endsWith("/argocd-axi context end"))))));
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
export function uninstallSessionEndHook() {
|
|
89
|
+
const value = settings();
|
|
90
|
+
const hooks = value.hooks;
|
|
91
|
+
if (!isRecord(hooks) || !Array.isArray(hooks.SessionEnd))
|
|
92
|
+
return false;
|
|
93
|
+
let removed = false;
|
|
94
|
+
const next = hooks.SessionEnd.flatMap((group) => {
|
|
95
|
+
if (!isRecord(group) || !Array.isArray(group.hooks))
|
|
96
|
+
return [group];
|
|
97
|
+
const entries = group.hooks.filter((entry) => {
|
|
98
|
+
const ours = isRecord(entry) &&
|
|
99
|
+
(entry.command === command ||
|
|
100
|
+
(typeof entry.command === "string" &&
|
|
101
|
+
entry.command.endsWith("/argocd-axi context end")));
|
|
102
|
+
removed ||= ours;
|
|
103
|
+
return !ours;
|
|
104
|
+
});
|
|
105
|
+
return entries.length ? [{ ...group, hooks: entries }] : [];
|
|
106
|
+
});
|
|
107
|
+
if (!removed)
|
|
108
|
+
return false;
|
|
109
|
+
if (next.length)
|
|
110
|
+
hooks.SessionEnd = next;
|
|
111
|
+
else
|
|
112
|
+
delete hooks.SessionEnd;
|
|
113
|
+
if (Object.keys(hooks).length === 0)
|
|
114
|
+
delete value.hooks;
|
|
115
|
+
writeSettings(value);
|
|
116
|
+
return true;
|
|
117
|
+
}
|
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
import { chmodSync, closeSync, createReadStream, mkdirSync, openSync, readdirSync, readFileSync, renameSync, rmdirSync, statSync, unlinkSync, writeFileSync, } from "node:fs";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
import { createInterface } from "node:readline";
|
|
5
|
+
import { configPath } from "./config.js";
|
|
6
|
+
const MAX_DIRECTORIES = 100;
|
|
7
|
+
const LOCK_WAIT_MS = 2_000;
|
|
8
|
+
const STALE_LOCK_MS = 30_000;
|
|
9
|
+
const MAX_TRANSCRIPT_BYTES = 256 * 1024 * 1024;
|
|
10
|
+
const TOOL = "argocd-axi";
|
|
11
|
+
const COMMANDS = new Set([
|
|
12
|
+
"apps",
|
|
13
|
+
"projects",
|
|
14
|
+
"appsets",
|
|
15
|
+
"repos",
|
|
16
|
+
"clusters",
|
|
17
|
+
"ctx",
|
|
18
|
+
"auth",
|
|
19
|
+
"whoami",
|
|
20
|
+
"can-i",
|
|
21
|
+
"setup",
|
|
22
|
+
"context",
|
|
23
|
+
]);
|
|
24
|
+
const CONTEXTS = new Set([
|
|
25
|
+
"apps",
|
|
26
|
+
"projects",
|
|
27
|
+
"appsets",
|
|
28
|
+
"repos",
|
|
29
|
+
"clusters",
|
|
30
|
+
"ctx",
|
|
31
|
+
"auth",
|
|
32
|
+
"whoami",
|
|
33
|
+
"can-i",
|
|
34
|
+
]);
|
|
35
|
+
/** Split shell words without retaining anything in the persisted result. */
|
|
36
|
+
function words(command) {
|
|
37
|
+
const result = [];
|
|
38
|
+
let token = "";
|
|
39
|
+
let quote = "";
|
|
40
|
+
let started = false;
|
|
41
|
+
const end = () => {
|
|
42
|
+
if (started)
|
|
43
|
+
result.push(token);
|
|
44
|
+
token = "";
|
|
45
|
+
started = false;
|
|
46
|
+
};
|
|
47
|
+
for (let i = 0; i < command.length; i++) {
|
|
48
|
+
const c = command[i];
|
|
49
|
+
if (quote) {
|
|
50
|
+
if (c === quote)
|
|
51
|
+
quote = "";
|
|
52
|
+
else if (c === "\\" && quote === '"' && i + 1 < command.length)
|
|
53
|
+
token += command[++i];
|
|
54
|
+
else
|
|
55
|
+
token += c;
|
|
56
|
+
}
|
|
57
|
+
else if (c === "'" || c === '"') {
|
|
58
|
+
quote = c;
|
|
59
|
+
started = true;
|
|
60
|
+
}
|
|
61
|
+
else if (c === "\\" && i + 1 < command.length) {
|
|
62
|
+
token += command[++i];
|
|
63
|
+
started = true;
|
|
64
|
+
}
|
|
65
|
+
else if (/\s|[;|&()`]/.test(c))
|
|
66
|
+
end();
|
|
67
|
+
else {
|
|
68
|
+
token += c;
|
|
69
|
+
started = true;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
end();
|
|
73
|
+
return result;
|
|
74
|
+
}
|
|
75
|
+
function commandParts(line) {
|
|
76
|
+
const parts = [];
|
|
77
|
+
let quote = "";
|
|
78
|
+
let start = 0;
|
|
79
|
+
for (let i = 0; i < line.length; i++) {
|
|
80
|
+
const c = line[i];
|
|
81
|
+
if (quote) {
|
|
82
|
+
if (c === "\\" && quote === '"')
|
|
83
|
+
i++;
|
|
84
|
+
else if (c === quote)
|
|
85
|
+
quote = "";
|
|
86
|
+
}
|
|
87
|
+
else if (c === "\\")
|
|
88
|
+
i++;
|
|
89
|
+
else if (c === "'" || c === '"')
|
|
90
|
+
quote = c;
|
|
91
|
+
else if (/[;|&\n]/.test(c)) {
|
|
92
|
+
parts.push(line.slice(start, i));
|
|
93
|
+
start = i + 1;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
parts.push(line.slice(start));
|
|
97
|
+
return parts;
|
|
98
|
+
}
|
|
99
|
+
function commandCalls(line) {
|
|
100
|
+
const calls = [];
|
|
101
|
+
for (const part of commandParts(line)) {
|
|
102
|
+
const args = words(part);
|
|
103
|
+
const index = args.findIndex((word) => word.split("/").pop()?.replace(/\.js$/, "") === TOOL);
|
|
104
|
+
if (index < 0)
|
|
105
|
+
continue;
|
|
106
|
+
const argsAfter = args.slice(index + 1);
|
|
107
|
+
const command = argsAfter.find((arg) => !arg.startsWith("-"));
|
|
108
|
+
if (!command || !COMMANDS.has(command))
|
|
109
|
+
continue;
|
|
110
|
+
const contextArg = argsAfter.findIndex((arg) => arg === "--context");
|
|
111
|
+
const contextValue = contextArg >= 0
|
|
112
|
+
? argsAfter[contextArg + 1]
|
|
113
|
+
: argsAfter.find((arg) => arg.startsWith("--context="))?.slice(10);
|
|
114
|
+
calls.push({
|
|
115
|
+
command,
|
|
116
|
+
...(contextValue && CONTEXTS.has(command.split(" ")[0])
|
|
117
|
+
? { context: contextValue }
|
|
118
|
+
: {}),
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
return calls;
|
|
122
|
+
}
|
|
123
|
+
function isRecord(value) {
|
|
124
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
125
|
+
}
|
|
126
|
+
function* toolCommands(value) {
|
|
127
|
+
if (Array.isArray(value)) {
|
|
128
|
+
for (const item of value)
|
|
129
|
+
yield* toolCommands(item);
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
if (!isRecord(value))
|
|
133
|
+
return;
|
|
134
|
+
if (value.type === "tool_use") {
|
|
135
|
+
if (isRecord(value.input) && typeof value.input.command === "string")
|
|
136
|
+
yield value.input.command;
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
for (const key of ["message", "content"])
|
|
140
|
+
if (key in value)
|
|
141
|
+
yield* toolCommands(value[key]);
|
|
142
|
+
}
|
|
143
|
+
export async function summarizeTranscript(path) {
|
|
144
|
+
if (statSync(path).size > MAX_TRANSCRIPT_BYTES)
|
|
145
|
+
return undefined;
|
|
146
|
+
const counts = {};
|
|
147
|
+
const contexts = [];
|
|
148
|
+
const lines = createInterface({
|
|
149
|
+
input: createReadStream(path, { encoding: "utf8" }),
|
|
150
|
+
crlfDelay: Infinity,
|
|
151
|
+
});
|
|
152
|
+
for await (const line of lines) {
|
|
153
|
+
if (!line.includes(TOOL))
|
|
154
|
+
continue;
|
|
155
|
+
let entry;
|
|
156
|
+
try {
|
|
157
|
+
entry = JSON.parse(line);
|
|
158
|
+
}
|
|
159
|
+
catch {
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
for (const commandLine of toolCommands(entry))
|
|
163
|
+
for (const call of commandCalls(commandLine)) {
|
|
164
|
+
counts[call.command] = (counts[call.command] ?? 0) + 1;
|
|
165
|
+
if (call.context)
|
|
166
|
+
contexts.push(call.context);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
if (Object.keys(counts).length === 0)
|
|
170
|
+
return undefined;
|
|
171
|
+
const contextCounts = new Map();
|
|
172
|
+
for (const context of contexts)
|
|
173
|
+
contextCounts.set(context, (contextCounts.get(context) ?? 0) + 1);
|
|
174
|
+
const context = [...contextCounts].sort((a, b) => b[1] - a[1])[0]?.[0];
|
|
175
|
+
return { commands: counts, ...(context ? { context } : {}) };
|
|
176
|
+
}
|
|
177
|
+
const sessionsPath = () => join(dirname(configPath()), "sessions.json");
|
|
178
|
+
function isSessionSummary(value) {
|
|
179
|
+
return (isRecord(value) &&
|
|
180
|
+
isRecord(value.commands) &&
|
|
181
|
+
Object.entries(value.commands).every(([name, count]) => COMMANDS.has(name) &&
|
|
182
|
+
typeof count === "number" &&
|
|
183
|
+
Number.isSafeInteger(count) &&
|
|
184
|
+
count > 0) &&
|
|
185
|
+
(value.context === undefined ||
|
|
186
|
+
(typeof value.context === "string" && !/[\r\n]/.test(value.context))));
|
|
187
|
+
}
|
|
188
|
+
function readSessions() {
|
|
189
|
+
try {
|
|
190
|
+
const parsed = JSON.parse(readFileSync(sessionsPath(), "utf8"));
|
|
191
|
+
if (isRecord(parsed) && isRecord(parsed.directories))
|
|
192
|
+
return Object.fromEntries(Object.entries(parsed.directories).filter((entry) => isSessionSummary(entry[1])));
|
|
193
|
+
}
|
|
194
|
+
catch {
|
|
195
|
+
/* Missing or corrupt memory is optional. */
|
|
196
|
+
}
|
|
197
|
+
return {};
|
|
198
|
+
}
|
|
199
|
+
function processAlive(pid) {
|
|
200
|
+
try {
|
|
201
|
+
process.kill(pid, 0);
|
|
202
|
+
return true;
|
|
203
|
+
}
|
|
204
|
+
catch (error) {
|
|
205
|
+
return errorCode(error) !== "ESRCH";
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
function lockPid(token) {
|
|
209
|
+
const pid = Number(token.trim().split(/\s+/, 1)[0]);
|
|
210
|
+
return Number.isSafeInteger(pid) && pid > 0 ? pid : undefined;
|
|
211
|
+
}
|
|
212
|
+
function errorCode(error) {
|
|
213
|
+
return error &&
|
|
214
|
+
typeof error === "object" &&
|
|
215
|
+
"code" in error &&
|
|
216
|
+
typeof error.code === "string"
|
|
217
|
+
? error.code
|
|
218
|
+
: undefined;
|
|
219
|
+
}
|
|
220
|
+
async function acquireLock(path) {
|
|
221
|
+
const lockPath = `${path}.lock`;
|
|
222
|
+
const deadline = Date.now() + LOCK_WAIT_MS;
|
|
223
|
+
while (Date.now() < deadline) {
|
|
224
|
+
try {
|
|
225
|
+
mkdirSync(lockPath, 0o700);
|
|
226
|
+
const ownerPath = join(lockPath, `owner-${randomUUID()}`);
|
|
227
|
+
const token = `${process.pid} ${randomUUID()}\n`;
|
|
228
|
+
let fd;
|
|
229
|
+
try {
|
|
230
|
+
fd = openSync(ownerPath, "wx", 0o600);
|
|
231
|
+
}
|
|
232
|
+
catch {
|
|
233
|
+
rmdirSync(lockPath);
|
|
234
|
+
return undefined;
|
|
235
|
+
}
|
|
236
|
+
try {
|
|
237
|
+
writeFileSync(fd, token);
|
|
238
|
+
return { fd, path: ownerPath, token };
|
|
239
|
+
}
|
|
240
|
+
catch {
|
|
241
|
+
closeSync(fd);
|
|
242
|
+
try {
|
|
243
|
+
unlinkSync(ownerPath);
|
|
244
|
+
rmdirSync(lockPath);
|
|
245
|
+
}
|
|
246
|
+
catch {
|
|
247
|
+
/* The lock may already be gone. */
|
|
248
|
+
}
|
|
249
|
+
return undefined;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
catch (error) {
|
|
253
|
+
if (errorCode(error) !== "EEXIST")
|
|
254
|
+
return undefined;
|
|
255
|
+
if (reclaimStaleLock(lockPath))
|
|
256
|
+
continue;
|
|
257
|
+
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
return undefined;
|
|
261
|
+
}
|
|
262
|
+
function reclaimStaleLock(lockPath) {
|
|
263
|
+
try {
|
|
264
|
+
const owners = readdirSync(lockPath);
|
|
265
|
+
if (owners.length === 0) {
|
|
266
|
+
if (Date.now() - statSync(lockPath).mtimeMs <= STALE_LOCK_MS)
|
|
267
|
+
return false;
|
|
268
|
+
rmdirSync(lockPath);
|
|
269
|
+
return true;
|
|
270
|
+
}
|
|
271
|
+
for (const ownerFile of owners) {
|
|
272
|
+
const ownerPath = join(lockPath, ownerFile);
|
|
273
|
+
const info = statSync(ownerPath);
|
|
274
|
+
const owner = readFileSync(ownerPath, "utf8");
|
|
275
|
+
const pid = lockPid(owner);
|
|
276
|
+
if (Date.now() - info.mtimeMs <= STALE_LOCK_MS ||
|
|
277
|
+
pid === undefined ||
|
|
278
|
+
processAlive(pid) ||
|
|
279
|
+
readFileSync(ownerPath, "utf8") !== owner)
|
|
280
|
+
continue;
|
|
281
|
+
unlinkSync(ownerPath);
|
|
282
|
+
rmdirSync(lockPath);
|
|
283
|
+
return true;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
catch {
|
|
287
|
+
return false;
|
|
288
|
+
}
|
|
289
|
+
return false;
|
|
290
|
+
}
|
|
291
|
+
export function releaseSessionLock(lock) {
|
|
292
|
+
closeSync(lock.fd);
|
|
293
|
+
try {
|
|
294
|
+
if (readFileSync(lock.path, "utf8") === lock.token) {
|
|
295
|
+
unlinkSync(lock.path);
|
|
296
|
+
rmdirSync(dirname(lock.path));
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
catch {
|
|
300
|
+
/* A successor owns the lock, or it is already gone. */
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
export async function recordSession(directory, summary) {
|
|
304
|
+
const path = sessionsPath();
|
|
305
|
+
mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
|
|
306
|
+
const lock = await acquireLock(path);
|
|
307
|
+
if (lock === undefined)
|
|
308
|
+
return false;
|
|
309
|
+
try {
|
|
310
|
+
const directories = readSessions();
|
|
311
|
+
delete directories[directory];
|
|
312
|
+
directories[directory] = summary;
|
|
313
|
+
for (const old of Object.keys(directories).slice(0, Math.max(0, Object.keys(directories).length - MAX_DIRECTORIES)))
|
|
314
|
+
delete directories[old];
|
|
315
|
+
const temporary = `${path}.${randomUUID()}.tmp`;
|
|
316
|
+
try {
|
|
317
|
+
writeFileSync(temporary, `${JSON.stringify({ directories }, null, 2)}\n`, {
|
|
318
|
+
mode: 0o600,
|
|
319
|
+
});
|
|
320
|
+
chmodSync(temporary, 0o600);
|
|
321
|
+
renameSync(temporary, path);
|
|
322
|
+
}
|
|
323
|
+
catch (error) {
|
|
324
|
+
try {
|
|
325
|
+
unlinkSync(temporary);
|
|
326
|
+
}
|
|
327
|
+
catch {
|
|
328
|
+
/* No temporary file to remove. */
|
|
329
|
+
}
|
|
330
|
+
throw error;
|
|
331
|
+
}
|
|
332
|
+
return true;
|
|
333
|
+
}
|
|
334
|
+
finally {
|
|
335
|
+
releaseSessionLock(lock);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
export function lastSession(directory) {
|
|
339
|
+
return readSessions()[directory];
|
|
340
|
+
}
|
|
341
|
+
export function sessionText(summary) {
|
|
342
|
+
const commands = Object.entries(summary.commands)
|
|
343
|
+
.sort((a, b) => b[1] - a[1])
|
|
344
|
+
.map(([name, count]) => `${name} ${count}`)
|
|
345
|
+
.join("; ");
|
|
346
|
+
return `${commands}${summary.context ? ` - context ${JSON.stringify(summary.context)}` : ""}`;
|
|
347
|
+
}
|
package/dist/src/version.js
CHANGED
package/package.json
CHANGED
|
@@ -34,9 +34,11 @@ Run `npx -y argocd-axi auth guide` for offline instructions. A human operator lo
|
|
|
34
34
|
## Commands
|
|
35
35
|
|
|
36
36
|
```
|
|
37
|
-
commands[
|
|
37
|
+
commands[27]:
|
|
38
38
|
(no command): live overview - context, server, user, apps needing attention with the reason, running operations
|
|
39
39
|
ctx [--context <c>]: current context, server and CLI version, local only (what the session hook prints)
|
|
40
|
+
context: last session's argocd-axi command summary for this directory
|
|
41
|
+
context end: record the current Claude Code session summary (SessionEnd hook)
|
|
40
42
|
ctx use <context>: switch the argocd CLI's current context (local config only)
|
|
41
43
|
auth guide [--context <c>]: offline human login and agent token setup instructions
|
|
42
44
|
whoami: user, issuer and groups of the current credential
|
|
@@ -59,7 +61,7 @@ commands[25]:
|
|
|
59
61
|
projects | projects view <p> | projects windows <p>: projects, their rules, and whether sync is allowed now
|
|
60
62
|
appsets | appsets view <name> | appsets generate <file>: ApplicationSets and what a spec would generate
|
|
61
63
|
repos | clusters: repositories and destination clusters with connection status
|
|
62
|
-
setup hooks:
|
|
64
|
+
setup hooks | status | uninstall: manage the Claude Code SessionEnd memory hook; setup hooks also installs SessionStart hooks
|
|
63
65
|
```
|
|
64
66
|
|
|
65
67
|
Run `npx -y argocd-axi --help` for global flags, or `npx -y argocd-axi <command> --help` for per-command usage.
|