@stzhu/skills 0.1.0
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/LICENSE +21 -0
- package/README.md +49 -0
- package/dist/bash-complete.mjs +14 -0
- package/dist/cli.mjs +9 -0
- package/dist/context-SQBI_fVn.mjs +102 -0
- package/dist/stats-RlcmzM9e.mjs +341 -0
- package/dist/validate-D3AdsB3Q.mjs +104 -0
- package/package.json +59 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Steve Zhu
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# @stzhu/skills
|
|
2
|
+
|
|
3
|
+
A collection of CLI tools (skills) for AI agents and developers.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pnpm add @stzhu/skills
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
The CLI is available as `stz-skills`. You can also run it via `pnpx`:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
pnpx @stzhu/skills <command>
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
### Agent Logbook
|
|
20
|
+
|
|
21
|
+
Manage and validate AI agent activity logs in the `.agent-logbook/` directory.
|
|
22
|
+
|
|
23
|
+
#### Get Session Stats
|
|
24
|
+
|
|
25
|
+
Retrieve token usage and model information for a specific agent session.
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
# For Claude Code
|
|
29
|
+
pnpx @stzhu/skills agent-logbook stats <session-id> --agent claudecode
|
|
30
|
+
|
|
31
|
+
# For Gemini CLI
|
|
32
|
+
pnpx @stzhu/skills agent-logbook stats <session-id> --agent geminicli
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
#### Validate Logbook Files
|
|
36
|
+
|
|
37
|
+
Validate the filenames and frontmatter of your logbook entries.
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
# Validate all files in .agent-logbook/
|
|
41
|
+
pnpx @stzhu/skills agent-logbook validate
|
|
42
|
+
|
|
43
|
+
# Validate a specific file or directory
|
|
44
|
+
pnpx @stzhu/skills agent-logbook validate .agent-logbook/activity/
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## License
|
|
48
|
+
|
|
49
|
+
MIT
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { n as app, t as buildContext } from "./context-SQBI_fVn.mjs";
|
|
3
|
+
import { proposeCompletions } from "@stricli/core";
|
|
4
|
+
|
|
5
|
+
//#region src/bin/bash-complete.ts
|
|
6
|
+
const inputs = process.argv.slice(3);
|
|
7
|
+
if (process.env["COMP_LINE"]?.endsWith(" ")) inputs.push("");
|
|
8
|
+
await proposeCompletions(app, inputs, buildContext(process));
|
|
9
|
+
try {
|
|
10
|
+
for (const { completion } of await proposeCompletions(app, inputs, buildContext(process))) process.stdout.write(`${completion}\n`);
|
|
11
|
+
} catch {}
|
|
12
|
+
|
|
13
|
+
//#endregion
|
|
14
|
+
export { };
|
package/dist/cli.mjs
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { buildApplication, buildCommand, buildRouteMap } from "@stricli/core";
|
|
2
|
+
import { buildInstallCommand, buildUninstallCommand } from "@stricli/auto-complete";
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { findWorkspacesRoot } from "find-workspaces";
|
|
7
|
+
|
|
8
|
+
//#region package.json
|
|
9
|
+
var version = "0.1.0";
|
|
10
|
+
var description = "Skills CLI";
|
|
11
|
+
var bin = {
|
|
12
|
+
"__stz-skills_bash_complete": "dist/bash-complete.mjs",
|
|
13
|
+
"stz-skills": "dist/cli.mjs"
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
//#endregion
|
|
17
|
+
//#region src/commands/agent-logbook/commands.ts
|
|
18
|
+
const statsCommand = buildCommand({
|
|
19
|
+
loader: async () => {
|
|
20
|
+
const { stats } = await import("./stats-RlcmzM9e.mjs");
|
|
21
|
+
return stats;
|
|
22
|
+
},
|
|
23
|
+
parameters: {
|
|
24
|
+
flags: { agent: {
|
|
25
|
+
kind: "enum",
|
|
26
|
+
brief: "The agent to get stats for",
|
|
27
|
+
values: ["claudecode", "geminicli"]
|
|
28
|
+
} },
|
|
29
|
+
positional: {
|
|
30
|
+
kind: "tuple",
|
|
31
|
+
parameters: [{
|
|
32
|
+
parse: String,
|
|
33
|
+
brief: "The session ID to get stats for"
|
|
34
|
+
}]
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
docs: { brief: "Agent logbook stats command" }
|
|
38
|
+
});
|
|
39
|
+
const validateCommand = buildCommand({
|
|
40
|
+
loader: async () => {
|
|
41
|
+
const { validate } = await import("./validate-D3AdsB3Q.mjs");
|
|
42
|
+
return validate;
|
|
43
|
+
},
|
|
44
|
+
parameters: { positional: {
|
|
45
|
+
kind: "tuple",
|
|
46
|
+
parameters: [{
|
|
47
|
+
parse: String,
|
|
48
|
+
brief: "The target file or directory to validate frontmatter for",
|
|
49
|
+
optional: true
|
|
50
|
+
}]
|
|
51
|
+
} },
|
|
52
|
+
docs: { brief: "Agent logbook validate command" }
|
|
53
|
+
});
|
|
54
|
+
const agentLogbookRoutes = buildRouteMap({
|
|
55
|
+
routes: {
|
|
56
|
+
stats: statsCommand,
|
|
57
|
+
validate: validateCommand
|
|
58
|
+
},
|
|
59
|
+
docs: { brief: "Agent logbook commands" }
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
//#endregion
|
|
63
|
+
//#region src/app.ts
|
|
64
|
+
const routes = buildRouteMap({
|
|
65
|
+
routes: {
|
|
66
|
+
"agent-logbook": agentLogbookRoutes,
|
|
67
|
+
install: buildInstallCommand("stz-skills", { bash: "__stz-skills_bash_complete" }),
|
|
68
|
+
uninstall: buildUninstallCommand("stz-skills", { bash: true })
|
|
69
|
+
},
|
|
70
|
+
docs: {
|
|
71
|
+
brief: description,
|
|
72
|
+
hideRoute: {
|
|
73
|
+
install: true,
|
|
74
|
+
uninstall: true
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
const app = buildApplication(routes, {
|
|
79
|
+
name: (() => {
|
|
80
|
+
const name = Object.keys(bin).find((key) => !key.startsWith("__"));
|
|
81
|
+
if (!name) throw new Error("Name not found");
|
|
82
|
+
return name;
|
|
83
|
+
})(),
|
|
84
|
+
versionInfo: { currentVersion: version }
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
//#endregion
|
|
88
|
+
//#region src/context.ts
|
|
89
|
+
function buildContext(process) {
|
|
90
|
+
const workspacesRoot = findWorkspacesRoot();
|
|
91
|
+
if (!workspacesRoot) throw new Error("Not in an npm project");
|
|
92
|
+
return {
|
|
93
|
+
process,
|
|
94
|
+
os,
|
|
95
|
+
fs,
|
|
96
|
+
path,
|
|
97
|
+
workspacesRoot: workspacesRoot.location
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
//#endregion
|
|
102
|
+
export { app as n, buildContext as t };
|
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import readline from "node:readline";
|
|
5
|
+
|
|
6
|
+
//#region src/util/defineCommandFunction.ts
|
|
7
|
+
/**
|
|
8
|
+
* Identity wrapper for strongly typed command implementations.
|
|
9
|
+
*
|
|
10
|
+
* Keeps generic flag/arg/context types attached to the command function so
|
|
11
|
+
* call sites get full inference and editor hints.
|
|
12
|
+
*/
|
|
13
|
+
function defineCommandFunction(impl) {
|
|
14
|
+
return impl;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
//#endregion
|
|
18
|
+
//#region src/commands/agent-logbook/stats/formatSessionStatsOutput.ts
|
|
19
|
+
/**
|
|
20
|
+
* Formats a StatsResult into a human-readable table/report for the CLI.
|
|
21
|
+
*
|
|
22
|
+
* @param agentName Name of the agent (e.g., ClaudeCode).
|
|
23
|
+
* @param sessionId Unique ID of the session.
|
|
24
|
+
* @param result Aggregated data to display.
|
|
25
|
+
*/
|
|
26
|
+
function formatSessionStatsOutput(agentName, sessionId, result) {
|
|
27
|
+
const lines = [];
|
|
28
|
+
lines.push("");
|
|
29
|
+
lines.push(`${agentName} Session Stats: ${sessionId}`);
|
|
30
|
+
lines.push("========================================");
|
|
31
|
+
if (result.models.length > 0) {
|
|
32
|
+
const modelsMeta = result.meta?.filter(([label]) => label === "Models" || label === "");
|
|
33
|
+
if (modelsMeta && modelsMeta.length > 0) {
|
|
34
|
+
for (const [label, value] of modelsMeta) if (label === "Models") lines.push(`Models Used: ${value}`);
|
|
35
|
+
else if (label === "") lines.push(` ${value}`);
|
|
36
|
+
} else lines.push(`Models Used: ${result.models.join(", ") || "N/A"}`);
|
|
37
|
+
}
|
|
38
|
+
if (result.meta) {
|
|
39
|
+
for (const [label, value] of result.meta) if (label !== "Models" && label !== "") lines.push(`${label}: ${value}`);
|
|
40
|
+
}
|
|
41
|
+
for (const section of result.sections) {
|
|
42
|
+
lines.push("----------------------------------------");
|
|
43
|
+
lines.push(`${section.label}:`);
|
|
44
|
+
for (const [label, value] of section.entries) {
|
|
45
|
+
const num = typeof value === "number" ? value.toLocaleString() : String(value);
|
|
46
|
+
lines.push(` ${label.padEnd(20)} ${num}`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
if (result.summary) {
|
|
50
|
+
lines.push("----------------------------------------");
|
|
51
|
+
lines.push(`${result.summary.label}:`);
|
|
52
|
+
for (const [label, value] of result.summary.entries) {
|
|
53
|
+
const num = typeof value === "number" ? value.toLocaleString() : String(value);
|
|
54
|
+
lines.push(` ${label.padEnd(20)} ${num}`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
lines.push("----------------------------------------");
|
|
58
|
+
lines.push(`GRAND TOTAL TOKENS: ${typeof result.grandTotal === "number" ? result.grandTotal.toLocaleString() : result.grandTotal}`);
|
|
59
|
+
lines.push("========================================");
|
|
60
|
+
lines.push("");
|
|
61
|
+
return lines.join("\n");
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
//#endregion
|
|
65
|
+
//#region src/commands/agent-logbook/stats/defineSessionStatsPlugin.ts
|
|
66
|
+
/**
|
|
67
|
+
* Identity wrapper for strongly typed session stats plugin implementations.
|
|
68
|
+
*
|
|
69
|
+
* Keeps types attached to the plugin object so call sites get full inference
|
|
70
|
+
* and editor hints.
|
|
71
|
+
*/
|
|
72
|
+
function defineSessionStatsPlugin(plugin) {
|
|
73
|
+
return plugin;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
//#endregion
|
|
77
|
+
//#region src/commands/agent-logbook/stats/plugins/claudecode.ts
|
|
78
|
+
/** Base directory where Claude Code stores project and session metadata. */
|
|
79
|
+
const CLAUDECODE_PROJECTS_DIR = path.join(os.homedir(), ".claude", "projects");
|
|
80
|
+
/**
|
|
81
|
+
* Parses a single JSONL file from Claude's logs to extract token usage.
|
|
82
|
+
* Only 'assistant' message types with 'usage' fields are counted.
|
|
83
|
+
*
|
|
84
|
+
* @param filePath Path to the .jsonl file.
|
|
85
|
+
*/
|
|
86
|
+
async function parseJsonlFile(filePath) {
|
|
87
|
+
const stats = {
|
|
88
|
+
input_tokens: 0,
|
|
89
|
+
output_tokens: 0,
|
|
90
|
+
cache_creation_input_tokens: 0,
|
|
91
|
+
cache_read_input_tokens: 0,
|
|
92
|
+
models: /* @__PURE__ */ new Set()
|
|
93
|
+
};
|
|
94
|
+
const fileStream = fs.createReadStream(filePath);
|
|
95
|
+
const rl = readline.createInterface({
|
|
96
|
+
input: fileStream,
|
|
97
|
+
crlfDelay: Infinity
|
|
98
|
+
});
|
|
99
|
+
for await (const line of rl) try {
|
|
100
|
+
const data = JSON.parse(line);
|
|
101
|
+
if (data.type === "assistant" && data.message && data.message.usage) {
|
|
102
|
+
const usage = data.message.usage;
|
|
103
|
+
stats.input_tokens += usage.input_tokens || 0;
|
|
104
|
+
stats.output_tokens += usage.output_tokens || 0;
|
|
105
|
+
stats.cache_creation_input_tokens += usage.cache_creation_input_tokens || 0;
|
|
106
|
+
stats.cache_read_input_tokens += usage.cache_read_input_tokens || 0;
|
|
107
|
+
if (data.message.model) stats.models.add(data.message.model);
|
|
108
|
+
}
|
|
109
|
+
} catch {}
|
|
110
|
+
return stats;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Merges source stats into a target stats object.
|
|
114
|
+
* Used for summing main session and subagent totals.
|
|
115
|
+
*/
|
|
116
|
+
function combineStats(target, source) {
|
|
117
|
+
target.input_tokens += source.input_tokens;
|
|
118
|
+
target.output_tokens += source.output_tokens;
|
|
119
|
+
target.cache_creation_input_tokens += source.cache_creation_input_tokens;
|
|
120
|
+
target.cache_read_input_tokens += source.cache_read_input_tokens;
|
|
121
|
+
source.models.forEach((m) => target.models.add(m));
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* The ClaudeCode-specific stats plugin.
|
|
125
|
+
* Handles parsing ClaudeCode's .jsonl logs found in ~/.claude/projects.
|
|
126
|
+
*/
|
|
127
|
+
const claudecodePlugin = defineSessionStatsPlugin({
|
|
128
|
+
name: "claudecode",
|
|
129
|
+
async findSession(sessionId) {
|
|
130
|
+
try {
|
|
131
|
+
const sessionLookup = (await fs.promises.readdir(CLAUDECODE_PROJECTS_DIR)).map(async (projectDir) => {
|
|
132
|
+
const projectPath = path.join(CLAUDECODE_PROJECTS_DIR, projectDir);
|
|
133
|
+
if (!(await fs.promises.stat(projectPath)).isDirectory()) throw new Error(`Not a directory: ${projectPath}`);
|
|
134
|
+
const sessionFile = path.join(projectPath, `${sessionId}.jsonl`);
|
|
135
|
+
const subagentsDir = path.join(projectPath, sessionId, "subagents");
|
|
136
|
+
await fs.promises.access(sessionFile);
|
|
137
|
+
let subagentFiles = [];
|
|
138
|
+
try {
|
|
139
|
+
subagentFiles = (await fs.promises.readdir(subagentsDir)).filter((f) => f.endsWith(".jsonl")).map((f) => path.join(subagentsDir, f));
|
|
140
|
+
} catch {}
|
|
141
|
+
return {
|
|
142
|
+
sessionFile,
|
|
143
|
+
subagentFiles
|
|
144
|
+
};
|
|
145
|
+
});
|
|
146
|
+
try {
|
|
147
|
+
return await Promise.any(sessionLookup);
|
|
148
|
+
} catch (error) {
|
|
149
|
+
if (error instanceof AggregateError) return null;
|
|
150
|
+
throw error;
|
|
151
|
+
}
|
|
152
|
+
} catch (error) {
|
|
153
|
+
console.error("Error searching projects directory:", error.message);
|
|
154
|
+
}
|
|
155
|
+
return null;
|
|
156
|
+
},
|
|
157
|
+
async aggregateStats(sessionData) {
|
|
158
|
+
const mainStats = await parseJsonlFile(sessionData.sessionFile);
|
|
159
|
+
const totalStats = {
|
|
160
|
+
...mainStats,
|
|
161
|
+
models: new Set(mainStats.models)
|
|
162
|
+
};
|
|
163
|
+
const subagentsStats = {
|
|
164
|
+
input_tokens: 0,
|
|
165
|
+
output_tokens: 0,
|
|
166
|
+
cache_creation_input_tokens: 0,
|
|
167
|
+
cache_read_input_tokens: 0,
|
|
168
|
+
models: /* @__PURE__ */ new Set(),
|
|
169
|
+
count: sessionData.subagentFiles.length
|
|
170
|
+
};
|
|
171
|
+
const allSubagentStats = await Promise.all(sessionData.subagentFiles.map((file) => parseJsonlFile(file)));
|
|
172
|
+
for (const stats of allSubagentStats) {
|
|
173
|
+
combineStats(subagentsStats, stats);
|
|
174
|
+
combineStats(totalStats, stats);
|
|
175
|
+
}
|
|
176
|
+
const models = Array.from(totalStats.models);
|
|
177
|
+
const meta = [];
|
|
178
|
+
const sections = [{
|
|
179
|
+
label: "MAIN SESSION",
|
|
180
|
+
entries: [
|
|
181
|
+
["Input Tokens", mainStats.input_tokens],
|
|
182
|
+
["Output Tokens", mainStats.output_tokens],
|
|
183
|
+
["Cache Creation Input", mainStats.cache_creation_input_tokens],
|
|
184
|
+
["Cache Read Input", mainStats.cache_read_input_tokens]
|
|
185
|
+
]
|
|
186
|
+
}];
|
|
187
|
+
if (subagentsStats.count > 0) {
|
|
188
|
+
const modelLine = `Main: ${Array.from(mainStats.models).join(", ") || "N/A"}`;
|
|
189
|
+
const subLine = `Subagents: ${Array.from(subagentsStats.models).join(", ") || "N/A"}`;
|
|
190
|
+
meta.push(["Models", modelLine]);
|
|
191
|
+
meta.push(["", subLine]);
|
|
192
|
+
sections.push({
|
|
193
|
+
label: `SUBAGENTS (${subagentsStats.count} total)`,
|
|
194
|
+
entries: [
|
|
195
|
+
["Input Tokens", subagentsStats.input_tokens],
|
|
196
|
+
["Output Tokens", subagentsStats.output_tokens],
|
|
197
|
+
["Cache Creation Input", subagentsStats.cache_creation_input_tokens],
|
|
198
|
+
["Cache Read Input", subagentsStats.cache_read_input_tokens]
|
|
199
|
+
]
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
return {
|
|
203
|
+
models,
|
|
204
|
+
meta,
|
|
205
|
+
sections,
|
|
206
|
+
summary: {
|
|
207
|
+
label: "TOTAL USAGE",
|
|
208
|
+
entries: [
|
|
209
|
+
["Total Input Tokens", totalStats.input_tokens],
|
|
210
|
+
["Total Output Tokens", totalStats.output_tokens],
|
|
211
|
+
["Total Cache Creation", totalStats.cache_creation_input_tokens],
|
|
212
|
+
["Total Cache Read", totalStats.cache_read_input_tokens]
|
|
213
|
+
]
|
|
214
|
+
},
|
|
215
|
+
grandTotal: totalStats.input_tokens + totalStats.output_tokens + totalStats.cache_creation_input_tokens + totalStats.cache_read_input_tokens
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
//#endregion
|
|
221
|
+
//#region src/commands/agent-logbook/stats/plugins/geminicli.ts
|
|
222
|
+
/** Base directory where Gemini CLI stores its temporary session data. */
|
|
223
|
+
const GEMINICLI_TMP_DIR = path.join(os.homedir(), ".gemini", "tmp");
|
|
224
|
+
/**
|
|
225
|
+
* Helper to read and parse a JSON file from the filesystem.
|
|
226
|
+
* @param filePath Path to the .json file.
|
|
227
|
+
*/
|
|
228
|
+
async function readJsonFile(filePath) {
|
|
229
|
+
const content = await fs.promises.readFile(filePath, "utf8");
|
|
230
|
+
return JSON.parse(content);
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* The GeminiCLI-specific stats plugin.
|
|
234
|
+
* Handles parsing GeminiCLI session JSON files found in ~/.gemini/tmp.
|
|
235
|
+
*/
|
|
236
|
+
const geminicliPlugin = defineSessionStatsPlugin({
|
|
237
|
+
name: "geminicli",
|
|
238
|
+
async findSession(sessionId) {
|
|
239
|
+
const matchingFiles = [];
|
|
240
|
+
try {
|
|
241
|
+
const projectDirs = await fs.promises.readdir(GEMINICLI_TMP_DIR);
|
|
242
|
+
const dirResults = await Promise.all(projectDirs.map(async (projectDir) => {
|
|
243
|
+
const chatsDir = path.join(GEMINICLI_TMP_DIR, projectDir, "chats");
|
|
244
|
+
try {
|
|
245
|
+
const jsonFiles = (await fs.promises.readdir(chatsDir)).filter((f) => f.endsWith(".json")).map((f) => path.join(chatsDir, f));
|
|
246
|
+
return (await Promise.all(jsonFiles.map(async (filePath) => {
|
|
247
|
+
return (await readJsonFile(filePath)).sessionId === sessionId ? filePath : null;
|
|
248
|
+
}))).filter((f) => f !== null);
|
|
249
|
+
} catch {
|
|
250
|
+
return [];
|
|
251
|
+
}
|
|
252
|
+
}));
|
|
253
|
+
matchingFiles.push(...dirResults.flat());
|
|
254
|
+
} catch (error) {
|
|
255
|
+
console.error("Error searching gemini tmp directory:", error.message);
|
|
256
|
+
}
|
|
257
|
+
return matchingFiles.length > 0 ? matchingFiles : null;
|
|
258
|
+
},
|
|
259
|
+
async aggregateStats(sessionFiles) {
|
|
260
|
+
const stats = {
|
|
261
|
+
input: 0,
|
|
262
|
+
output: 0,
|
|
263
|
+
cached: 0,
|
|
264
|
+
thoughts: 0,
|
|
265
|
+
tool: 0,
|
|
266
|
+
total: 0,
|
|
267
|
+
models: /* @__PURE__ */ new Set()
|
|
268
|
+
};
|
|
269
|
+
const results = await Promise.all(sessionFiles.map(async (filePath) => {
|
|
270
|
+
try {
|
|
271
|
+
return await readJsonFile(filePath);
|
|
272
|
+
} catch (error) {
|
|
273
|
+
console.error(`Error processing file ${filePath}:`, error.message);
|
|
274
|
+
return null;
|
|
275
|
+
}
|
|
276
|
+
}));
|
|
277
|
+
for (const data of results) {
|
|
278
|
+
if (!data?.messages || !Array.isArray(data.messages)) continue;
|
|
279
|
+
for (const msg of data.messages) if (msg.type === "gemini" && msg.tokens) {
|
|
280
|
+
stats.input += msg.tokens.input || 0;
|
|
281
|
+
stats.output += msg.tokens.output || 0;
|
|
282
|
+
stats.cached += msg.tokens.cached || 0;
|
|
283
|
+
stats.thoughts += msg.tokens.thoughts || 0;
|
|
284
|
+
stats.tool += msg.tokens.tool || 0;
|
|
285
|
+
stats.total += msg.tokens.total || 0;
|
|
286
|
+
if (msg.model) stats.models.add(msg.model);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
return {
|
|
290
|
+
models: Array.from(stats.models),
|
|
291
|
+
meta: [["Files Found", sessionFiles.length]],
|
|
292
|
+
sections: [{
|
|
293
|
+
label: "TOKEN USAGE",
|
|
294
|
+
entries: [
|
|
295
|
+
["Input Tokens", stats.input - stats.cached],
|
|
296
|
+
["Output Tokens", stats.output],
|
|
297
|
+
["Cached Tokens", stats.cached],
|
|
298
|
+
["Thoughts Tokens", stats.thoughts],
|
|
299
|
+
["Tool Tokens", stats.tool]
|
|
300
|
+
]
|
|
301
|
+
}],
|
|
302
|
+
summary: null,
|
|
303
|
+
grandTotal: stats.total
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
//#endregion
|
|
309
|
+
//#region src/commands/agent-logbook/stats/index.ts
|
|
310
|
+
/** Mapping of agent names to their respective stats plugins. */
|
|
311
|
+
const plugins = {
|
|
312
|
+
claudecode: claudecodePlugin,
|
|
313
|
+
geminicli: geminicliPlugin
|
|
314
|
+
};
|
|
315
|
+
/**
|
|
316
|
+
* The 'stats' command implementation.
|
|
317
|
+
* Retrieves and displays aggregated token usage for a given agent session.
|
|
318
|
+
*/
|
|
319
|
+
const stats = defineCommandFunction(async function stats({ agent }, sessionId) {
|
|
320
|
+
if (!agent || !sessionId) {
|
|
321
|
+
console.error("Usage: session-stats.js <agent> <session-id>");
|
|
322
|
+
console.error("Agents: claudecode, geminicli");
|
|
323
|
+
process.exit(1);
|
|
324
|
+
}
|
|
325
|
+
const plugin = plugins[agent];
|
|
326
|
+
if (!plugin) {
|
|
327
|
+
console.error(`Unknown agent: ${agent}`);
|
|
328
|
+
console.error("Available agents: claudecode, geminicli");
|
|
329
|
+
process.exit(1);
|
|
330
|
+
}
|
|
331
|
+
const sessionData = await plugin.findSession(sessionId);
|
|
332
|
+
if (!sessionData) {
|
|
333
|
+
console.error(`Session not found for ID: ${sessionId}`);
|
|
334
|
+
process.exit(1);
|
|
335
|
+
}
|
|
336
|
+
const result = await plugin.aggregateStats(sessionData);
|
|
337
|
+
console.log(formatSessionStatsOutput(plugin.name, sessionId, result));
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
//#endregion
|
|
341
|
+
export { stats };
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import matter from "gray-matter";
|
|
4
|
+
import { Compile } from "typebox/compile";
|
|
5
|
+
import yaml from "yaml";
|
|
6
|
+
import { Type } from "typebox";
|
|
7
|
+
|
|
8
|
+
//#region src/commands/agent-logbook/validate/frontmatterSchema.ts
|
|
9
|
+
const IsoDate = Type.Codec(Type.String({ format: "date-time" })).Decode((value) => new Date(value)).Encode((value) => value.toISOString());
|
|
10
|
+
/**
|
|
11
|
+
* TypeBox schema for agent logbook frontmatter.
|
|
12
|
+
*/
|
|
13
|
+
const FrontmatterSchema = Type.Object({
|
|
14
|
+
date: IsoDate,
|
|
15
|
+
type: Type.Union([
|
|
16
|
+
Type.Literal("activity"),
|
|
17
|
+
Type.Literal("research"),
|
|
18
|
+
Type.Literal("decision"),
|
|
19
|
+
Type.Literal("plan")
|
|
20
|
+
]),
|
|
21
|
+
status: Type.Union([
|
|
22
|
+
Type.Literal("complete"),
|
|
23
|
+
Type.Literal("in-progress"),
|
|
24
|
+
Type.Literal("abandoned"),
|
|
25
|
+
Type.Literal("success"),
|
|
26
|
+
Type.Literal("failure"),
|
|
27
|
+
Type.Literal("partial")
|
|
28
|
+
]),
|
|
29
|
+
agent: Type.String({ minLength: 1 }),
|
|
30
|
+
branch: Type.String({ minLength: 1 }),
|
|
31
|
+
models: Type.Array(Type.String(), { minItems: 1 }),
|
|
32
|
+
sessionId: Type.Optional(Type.String()),
|
|
33
|
+
taskId: Type.Optional(Type.String()),
|
|
34
|
+
cost: Type.Optional(Type.String()),
|
|
35
|
+
tags: Type.Optional(Type.Array(Type.String())),
|
|
36
|
+
filesModified: Type.Optional(Type.Array(Type.String())),
|
|
37
|
+
relatedPlan: Type.Optional(Type.String()),
|
|
38
|
+
migrated: Type.Optional(Type.Boolean())
|
|
39
|
+
}, { additionalProperties: false });
|
|
40
|
+
|
|
41
|
+
//#endregion
|
|
42
|
+
//#region src/commands/agent-logbook/validate/index.ts
|
|
43
|
+
/**
|
|
44
|
+
* Expected filename format: `YYYY-MM-DD_HHMMSSZ_<agent>_<task-slug>.md`
|
|
45
|
+
*
|
|
46
|
+
* - `[0-9]{4}-[0-9]{2}-[0-9]{2}` — date (YYYY-MM-DD)
|
|
47
|
+
* - `[0-9]{6}Z` — UTC time without separators (HHMMSS) + Z suffix
|
|
48
|
+
* - `[a-z][a-z0-9-]*` — agent name: starts with a letter, then letters/digits/hyphens
|
|
49
|
+
* - `[a-z][a-z0-9-]+` — task slug: same rules, but at least 2 characters
|
|
50
|
+
*
|
|
51
|
+
* Example: `2024-03-15_143022Z_claudecode_my-task.md`
|
|
52
|
+
*/
|
|
53
|
+
const FILENAME_RE = /^[0-9]{4}-[0-9]{2}-[0-9]{2}_[0-9]{6}Z_[a-z][a-z0-9-]*_[a-z][a-z0-9-]+\.md$/;
|
|
54
|
+
/**
|
|
55
|
+
* Recursively collects `.md` files under `targetPath`, excluding anything inside a `templates/`
|
|
56
|
+
* subdirectory. When `targetPath` is a single file, returns it directly.
|
|
57
|
+
*/
|
|
58
|
+
async function findMarkdownFiles(targetPath) {
|
|
59
|
+
if (!(await fs.stat(targetPath)).isDirectory()) return [targetPath];
|
|
60
|
+
const results = [];
|
|
61
|
+
const queue = [targetPath];
|
|
62
|
+
while (queue.length > 0) {
|
|
63
|
+
const dir = queue.shift();
|
|
64
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
65
|
+
for (const entry of entries) {
|
|
66
|
+
const fullPath = path.join(dir, entry.name);
|
|
67
|
+
if (entry.isDirectory()) {
|
|
68
|
+
if (entry.name === "templates") continue;
|
|
69
|
+
queue.push(fullPath);
|
|
70
|
+
} else if (entry.isFile() && entry.name.endsWith(".md")) results.push(fullPath);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return results.sort();
|
|
74
|
+
}
|
|
75
|
+
async function validate(_flags, targetPath = ".agent-logbook") {
|
|
76
|
+
const absoluteTargetPath = path.resolve(this.workspacesRoot, targetPath);
|
|
77
|
+
const typeboxValidate = Compile(FrontmatterSchema);
|
|
78
|
+
const files = await findMarkdownFiles(absoluteTargetPath);
|
|
79
|
+
let failed = 0;
|
|
80
|
+
const filePromises = files.map(async (file) => {
|
|
81
|
+
const filename = path.basename(file);
|
|
82
|
+
if (!FILENAME_RE.test(filename)) {
|
|
83
|
+
console.error(`FAIL (filename) ${file}`);
|
|
84
|
+
failed++;
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
const { data } = matter(await fs.readFile(file, "utf8"), { engines: { yaml: {
|
|
88
|
+
parse: (text) => yaml.parse(text),
|
|
89
|
+
stringify: (data) => yaml.stringify(data)
|
|
90
|
+
} } });
|
|
91
|
+
if (!typeboxValidate.Check(data)) {
|
|
92
|
+
console.error(`FAIL (schema) ${file}`);
|
|
93
|
+
const errors = typeboxValidate.Errors(data);
|
|
94
|
+
for (const error of errors) console.error(` ${error.instancePath || "(root)"} ${error.message}`);
|
|
95
|
+
failed++;
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
await Promise.all(filePromises);
|
|
99
|
+
if (failed > 0) return process.exit(1);
|
|
100
|
+
console.log("All files validated successfully");
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
//#endregion
|
|
104
|
+
export { validate };
|
package/package.json
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@stzhu/skills",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Skills CLI",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"skills"
|
|
7
|
+
],
|
|
8
|
+
"license": "MIT",
|
|
9
|
+
"author": "Steve Zhu <4130171+stevezhu@users.noreply.github.com>",
|
|
10
|
+
"repository": {
|
|
11
|
+
"type": "git",
|
|
12
|
+
"url": "git+https://github.com/stevezhu/eslint-config.git"
|
|
13
|
+
},
|
|
14
|
+
"bin": {
|
|
15
|
+
"__stz-skills_bash_complete": "dist/bash-complete.mjs",
|
|
16
|
+
"stz-skills": "dist/cli.mjs"
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"dist"
|
|
20
|
+
],
|
|
21
|
+
"type": "module",
|
|
22
|
+
"imports": {
|
|
23
|
+
"#*": "./src/*"
|
|
24
|
+
},
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"@stricli/auto-complete": "^1.2.6",
|
|
27
|
+
"@stricli/core": "^1.2.6",
|
|
28
|
+
"find-workspaces": "^0.3.1",
|
|
29
|
+
"gray-matter": "^4.0.3",
|
|
30
|
+
"typebox": "^1.1.5",
|
|
31
|
+
"yaml": "^2.8.2"
|
|
32
|
+
},
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"@stzhu/tsconfig": "^0.1.1",
|
|
35
|
+
"@types/node": "^25.3.3",
|
|
36
|
+
"oxfmt": "^0.36.0",
|
|
37
|
+
"oxlint": "^1.51.0",
|
|
38
|
+
"oxlint-tsgolint": "^0.16.0",
|
|
39
|
+
"tsdown": "^0.20.3",
|
|
40
|
+
"tsx": "^4.21.0",
|
|
41
|
+
"typescript": "^5.9.3",
|
|
42
|
+
"vitest": "^4.0.18"
|
|
43
|
+
},
|
|
44
|
+
"engines": {
|
|
45
|
+
"node": ">=25"
|
|
46
|
+
},
|
|
47
|
+
"scripts": {
|
|
48
|
+
"ai:claude": "claude",
|
|
49
|
+
"ai:gemini": "SEATBELT_PROFILE=custom gemini",
|
|
50
|
+
"build": "tsdown",
|
|
51
|
+
"format": "oxfmt",
|
|
52
|
+
"format:check": "oxfmt --check",
|
|
53
|
+
"lint": "pnpm run format:check && oxlint --type-aware",
|
|
54
|
+
"lint:fix": "oxlint --type-aware --fix",
|
|
55
|
+
"postinstall": "([ ! -d \"src\" ] || pnpm run build) && node dist/cli.mjs install",
|
|
56
|
+
"pretest": "tsc --noEmit",
|
|
57
|
+
"test": "vitest"
|
|
58
|
+
}
|
|
59
|
+
}
|