@codeursenior/pek 0.0.0-stage → 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.
@@ -0,0 +1,159 @@
1
+ import path from "node:path";
2
+ import { HttpError } from "./http/errors.js";
3
+ import { clientsForContextEntry, links, toolsForSkill, } from "./scan/metadata.js";
4
+ import { referenceResolver } from "./scan/references.js";
5
+ import { inside, slash } from "./scan/values.js";
6
+ export const modelPrices = [
7
+ {
8
+ id: "gpt-5.3-codex",
9
+ name: "GPT-5.3 Codex",
10
+ inputPerMillion: 1.75,
11
+ source: "https://developers.openai.com/api/docs/pricing",
12
+ verifiedAt: "2026-10-10",
13
+ },
14
+ {
15
+ id: "claude-sonnet-5.5",
16
+ name: "Claude Sonnet 5.5",
17
+ inputPerMillion: 2,
18
+ source: "https://platform.claude.com/docs/en/about-claude/pricing",
19
+ verifiedAt: "2026-10-10",
20
+ },
21
+ {
22
+ id: "claude-opus-5.5",
23
+ name: "Claude Opus 5.5",
24
+ inputPerMillion: 4,
25
+ source: "https://platform.claude.com/docs/en/about-claude/pricing",
26
+ verifiedAt: "2026-10-10",
27
+ },
28
+ ];
29
+ const clients = ["codex", "claude", "cursor", "copilot"];
30
+ export const estimationMethod = "Local heuristic: Unicode characters / 4, rounded up per entry. Words are whitespace-separated; sizes are UTF-8 bytes. Not a model tokenizer or live usage measurement.";
31
+ /** Counts discovered text only; references never promote their targets to startup. */
32
+ export function contextInventory(entries, data, env) {
33
+ const byAlias = new Map(entries.flatMap((entry) => entry.aliases.map((alias) => [alias, entry])));
34
+ const result = {
35
+ codex: [],
36
+ claude: [],
37
+ cursor: [],
38
+ copilot: [],
39
+ };
40
+ const resolveReference = referenceResolver(entries, env);
41
+ const nodes = new Map(data.nodes.map((node) => [node.id, node]));
42
+ const rules = new Map(data.rules.map((rule) => [rule.id, rule]));
43
+ const skills = new Map(data.skills.flatMap((skill) => skill.editTargets.map((target) => [target.id, skill])));
44
+ for (const client of clients) {
45
+ const rows = new Map();
46
+ const queue = new Set();
47
+ const add = (entry, kind, startup, text = entry.text, name = path.basename(entry.file), id = entry.id) => {
48
+ queue.add(entry);
49
+ const existing = rows.get(id);
50
+ if (existing) {
51
+ if (startup)
52
+ rows.set(id, {
53
+ ...existing,
54
+ portion: "startup",
55
+ path: env.display(entry.file, entry.scope),
56
+ });
57
+ return;
58
+ }
59
+ rows.set(id, {
60
+ id,
61
+ name,
62
+ path: env.display(entry.file, entry.scope),
63
+ scope: entry.scope,
64
+ kind,
65
+ portion: startup ? "startup" : "on-demand",
66
+ tokens: Math.ceil(Array.from(text).length / 4),
67
+ words: text.match(/\S+/gu)?.length ?? 0,
68
+ bytes: Buffer.byteLength(text),
69
+ inputCost: 0,
70
+ });
71
+ };
72
+ for (const entry of entries) {
73
+ const node = nodes.get(entry.id);
74
+ if (node?.kind === "instruction") {
75
+ const aliases = entry.aliases.filter((alias) => clientsForContextEntry(alias, entry.scope, env.root, env.codexHome).includes(client));
76
+ const effective = aliases.filter((alias) => !(client === "codex" &&
77
+ path.basename(alias) === "AGENTS.md" &&
78
+ byAlias.has(path.join(path.dirname(alias), "AGENTS.override.md"))));
79
+ if (effective.length)
80
+ add(entry, "instruction", effective.some((alias) => entry.scope === "user" ||
81
+ path.dirname(alias) === env.root ||
82
+ (client === "claude" &&
83
+ path.dirname(alias) === path.join(env.root, ".claude")) ||
84
+ (client === "copilot" &&
85
+ alias ===
86
+ path.join(env.root, ".github/copilot-instructions.md"))));
87
+ }
88
+ const rule = rules.get(entry.id);
89
+ if (rule?.clients.includes(client)) {
90
+ const unscoped = client === "cursor"
91
+ ? rule.alwaysApply || rule.legacy
92
+ : client === "claude"
93
+ ? !rule.pathsCondition.length
94
+ : rule.applyTo.includes("**") || rule.applyTo.includes("**/*");
95
+ const rootRule = entry.scope === "user" ||
96
+ entry.aliases.some((alias) => /^(?:\.cursorrules|\.(?:claude|cursor)\/rules\/|\.github\/instructions\/)/.test(slash(path.relative(env.root, alias))));
97
+ add(entry, "rule", unscoped && rootRule);
98
+ }
99
+ const skill = skills.get(entry.id);
100
+ if (skill?.clients.includes(client) &&
101
+ entry.aliases.some((alias) => toolsForSkill(slash(alias)).includes(client) ||
102
+ (client === "codex" &&
103
+ entry.scope === "user" &&
104
+ inside(path.join(env.codexHome, "skills"), alias)))) {
105
+ // Identical copies and symlink aliases represent one discovered skill.
106
+ const catalog = `${skill.name}\n${skill.description}`;
107
+ const startup = entry.scope === "user" ||
108
+ entry.aliases.some((alias) => {
109
+ const relative = slash(path.relative(env.root, alias));
110
+ return (/^\.(?:agents|codex|claude|cursor|github)\/skills\//.test(relative) && toolsForSkill(relative).includes(client));
111
+ });
112
+ add(entry, "skill-catalog", startup, catalog, skill.name, `${skill.id}:skill-catalog`);
113
+ add(entry, "skill-body", false, entry.text.replace(/^---\r?\n[\s\S]*?\r?\n---(?:\r?\n|$)/, ""), skill.name, `${skill.id}:skill-body`);
114
+ }
115
+ }
116
+ // Sets preserve insertion order and visit newly discovered references, including cycles, once.
117
+ for (const entry of queue) {
118
+ for (const link of links(entry.text)) {
119
+ const found = resolveReference(entry, link);
120
+ if (!found ||
121
+ queue.has(found) ||
122
+ !env.allowed.some((base) => inside(base, found.real)))
123
+ continue;
124
+ add(found, "document", false);
125
+ }
126
+ }
127
+ result[client] = [...rows.values()].sort((a, b) => b.tokens - a.tokens || a.path.localeCompare(b.path));
128
+ }
129
+ return result;
130
+ }
131
+ export function estimateTokens(snapshot, clientId, modelId) {
132
+ const client = clients.find((client) => client === clientId);
133
+ const model = modelPrices.find((model) => model.id === modelId);
134
+ if (!client || !model)
135
+ throw new HttpError(400, "Select one supported agent and model.");
136
+ const entries = snapshot.tokenInventory[client].map((entry) => ({
137
+ ...entry,
138
+ inputCost: (entry.tokens * model.inputPerMillion) / 1_000_000,
139
+ }));
140
+ const total = (portion) => entries
141
+ .filter((entry) => entry.portion === portion)
142
+ .reduce((sum, entry) => ({
143
+ tokens: sum.tokens + entry.tokens,
144
+ words: sum.words + entry.words,
145
+ bytes: sum.bytes + entry.bytes,
146
+ inputCost: sum.inputCost + entry.inputCost,
147
+ }), { tokens: 0, words: 0, bytes: 0, inputCost: 0 });
148
+ return {
149
+ client,
150
+ model,
151
+ models: modelPrices,
152
+ method: estimationMethod,
153
+ entries,
154
+ startup: total("startup"),
155
+ onDemand: total("on-demand"),
156
+ includeUser: snapshot.data.limits.includeUser,
157
+ warnings: snapshot.data.warnings,
158
+ };
159
+ }
@@ -0,0 +1,47 @@
1
+ # Architecture
2
+
3
+ `src/cli.ts` parses arguments, starts the server for the requested folder and opens the default browser using OS launchers with argument arrays and no shell interpolation. TypeScript compiles it to `dist/cli.js`, retaining its executable shebang. The CLI resolves its version relative to the installed package, independently of the caller's working directory.
4
+
5
+ `src/scanner.ts` orchestrates a fresh snapshot. Modules under `src/scan/` handle filesystem collection, metadata parsing, context/rule/skill discovery, reference resolution and graph construction, and MCP discovery. Parsing starts with unknown values and narrows them before normalization. Context client discovery propagates across explicit reference edges, never folder-scope edges. Documents remain data, never instructions to the application. MCP output is an explicit metadata whitelist, never a serialized configuration.
6
+
7
+ `src/server.ts` owns the in-memory snapshot, coalesced refresh promise, and random session credential. Modules under `src/http/` handle authentication/security headers, serialized file writes, request validation, and browser asset enumeration. Requests select static files from an allowlist built from `dist/browser`; they never resolve arbitrary paths. Previews and edits use indexed opaque IDs, size limits, and content revision checks. CSP blocks remote code, frames, plugins and inline scripts. Each HTML response receives a fresh nonce for Angular component styles; critical CSS inlining is disabled.
8
+
9
+ `src/contracts.ts` defines browser-safe JSON shapes shared by the server and UI. Internal snapshot maps and real filesystem write targets live separately in `src/scan/types.ts`. These are internal contracts, not a published library API.
10
+
11
+ `src/token-estimator.ts` classifies local context per client and computes static volumes and one-load input costs. `src/optimization-checks.ts` runs fixed, bounded RTK identity probes; it never executes workspace-supplied commands or installs software. See [Token Optimizer](token-optimizer.md) for estimation assumptions, pricing sources and discovery limits.
12
+
13
+ The Angular UI uses standalone OnPush components and signals. `WorkspaceState` owns inventory, navigation, filters, theme and selection; `WorkspaceApi` owns authenticated HTTP requests. Components render the sidebar, filters, tables, graph, and detail editor. Angular text bindings display untrusted content without HTML interpretation. The session token moves from the URL fragment to tab session storage and is removed from the visible URL.
14
+
15
+ The detail component owns its edit draft and revision. A request sequence prevents late previews or writes from updating a different selection. The graph component delegates only its SVG subtree to the typed D3 renderer. Simulation nodes and links are copies of API data. Initial positions, layout forces and bounded warm-up are preserved; cleanup stops simulations and removes listeners when the graph is replaced or destroyed.
16
+
17
+ ## Extending and testing
18
+
19
+ - Add filesystem discovery behavior to the relevant scan module, with a synthetic fixture in the backend tests. Test normalization and reference rules as pure functions when possible; retain real filesystem tests for symlinks, scope boundaries, and limits.
20
+ - Change shared JSON contracts first when introducing API data. Add transport tests for new routes or file operations. Keep credentials and filesystem capabilities out of browser-visible types.
21
+ - Add UI behavior to its owning component or state service, with a Vitest test through Angular TestBed. Use `await fixture.whenStable()` after interactions rather than forced change detection. Keep HTTP calls behind `WorkspaceApi`.
22
+ - Keep D3-specific mutations inside the renderer. Validate gestures and production CSP in Playwright against the compiled server; do not rely on DOM emulation for SVG geometry.
23
+ - Run `npm run check` before committing. Browser baselines cover the original UI and both themes; update them only for reviewed visual changes. Coverage reports are diagnostic rather than percentage gates.
24
+
25
+ Development uses Node 24 and the Angular CLI's supported Vitest integration. Backend code is checked against Node 20 types and emitted as ESM; test/build tooling uses newer Node types separately. Angular and D3 are bundled into browser assets and are not installed as runtime dependencies. The npm archive includes only compiled output, documentation and licenses. Build caches, sources and fixtures stay out of it.
26
+
27
+ Client badges use local raster marks from [Cursor](https://cursor.com/brand), [Claude](https://claude.com/), and [ChatGPT](https://chatgpt.com/). The Codex discovery indicator uses the ChatGPT mark; Copilot uses a local text mark. The static asset allowlist keeps the images available without remote requests.
28
+
29
+ ## Discovery references
30
+
31
+ Behavior was checked against official documentation on 2026-09-30. Runtime behavior can differ by client release or configuration.
32
+
33
+ - [Codex skills](https://developers.openai.com/codex/skills/): repository and user `.agents/skills` discovery and symlink support.
34
+ - [Codex MCP](https://developers.openai.com/codex/mcp/): TOML server definitions and project/user configuration.
35
+ - [Claude Code skills](https://code.claude.com/docs/en/skills): `.claude/skills`, invocation metadata, and nested discovery.
36
+ - [Claude Code MCP](https://code.claude.com/docs/en/mcp): `.mcp.json` plus user and project-local entries in `~/.claude.json`.
37
+ - [Cursor skills](https://cursor.com/docs/skills): `.agents/skills`, `.cursor/skills`, and compatibility with Claude/Codex directories.
38
+ - [Cursor MCP](https://cursor.com/docs/context/model-context-protocol): `.cursor/mcp.json` project/user configuration.
39
+ - [Cursor rules](https://cursor.com/docs/context/rules): `.cursor/rules` and legacy `.cursorrules`.
40
+ - [Cursor rules help](https://prod.cursor.com/help/customization/rules): project-root `AGENTS.md` and `CLAUDE.md` are both read by Cursor.
41
+ - [Claude Code rules](https://code.claude.com/docs/en/memory): `.claude/rules` and `paths` frontmatter.
42
+ - [Codex instructions](https://learn.chatgpt.com/docs/agent-configuration/agents-md): `AGENTS.md` belongs in Context. [Codex command rules](https://learn.chatgpt.com/docs/agent-configuration/rules) are a separate execution policy.
43
+ - [Copilot instructions in VS Code](https://code.visualstudio.com/docs/agent-customization/custom-instructions): repository and personal instructions, `AGENTS.md`, and targeted `.instructions.md` files with `applyTo` metadata.
44
+ - [Copilot skills in VS Code](https://code.visualstudio.com/docs/agent-customization/agent-skills): project and personal skill locations and invocation metadata.
45
+ - [MCP servers in VS Code](https://code.visualstudio.com/docs/agent-customization/mcp-servers): workspace `.vscode/mcp.json`, portable `.mcp.json`, and Agent Host `~/.copilot/mcp-config.json`.
46
+
47
+ A live connection indicator needs an explicit integration with each client's running session. Executing commands found in arbitrary repository configuration is deliberately outside the viewer's scope. File-based discovery, configuration enablement, transport reachability and authenticated session connectivity are separate facts.
@@ -0,0 +1,56 @@
1
+ # Token Optimizer design
2
+
3
+ Status: implemented. The implementation request approved the confirmed decisions below.
4
+
5
+ ## Confirmed decisions
6
+
7
+ - Place Token Optimizer after MCP servers in the navigation, with a static context estimate represented as an iceberg. Keep the optimization checklist in its own following tab.
8
+ - Above water: applicable startup instruction contents and skill catalog entries, including names and descriptions.
9
+ - Below water: linked documents, skill bodies, and instructions applicable only in specific subfolders.
10
+ - Count a reference's text in its containing instruction file. Do not automatically include the referenced document's contents in the startup estimate.
11
+ - Require one selected agent: Codex, Claude Code, Cursor, or GitHub Copilot. Include its project and user context. Do not offer an aggregate "All clients" estimate.
12
+ - These estimates describe discoverable local context, not observed usage in a running conversation.
13
+ - Estimate the input cost of loading each iceberg portion once, using the selected model's input price without caching discounts.
14
+ - The on-demand price is hypothetical: it assumes all available on-demand content is loaded once. Neither price predicts session spending.
15
+ - Provide model presets with input prices maintained by the application. V1 has no custom model or manual price entry; users are not responsible for supplying pricing data.
16
+ - Include an extensible checklist starting with "Install RTK". Its checkbox reflects detected installation status, not a user's manual declaration.
17
+ - When RTK is detected, show the checkbox checked and disabled.
18
+ - When RTK is missing, show the checkbox unchecked and provide installation instructions with a "Check again" button. Do not install RTK from the application. Check and disable the checkbox only after detection succeeds.
19
+ - Show only above-water (startup) entries in the breakdown by file or skill, sorted by token count from largest to smallest, with word count and estimated input cost. Omit the byte-size column.
20
+ - Show context metrics and price estimates for both iceberg portions. Keep the visualization free of the illustration caption and repeated context/pricing caveats; retain the estimation method and official pricing in the expandable details.
21
+ - Exclude quality scores and rewrite suggestions from V1.
22
+
23
+ ## Implementation research
24
+
25
+ - RTK's [official installation guide](https://www.rtk-ai.app/docs/getting-started/installation/) documents `rtk --version` and `rtk gain` as installation checks. Version output alone is insufficient because another tool shares the executable name. Detection must verify Rust Token Killer specifically.
26
+ - Select and document a local token-estimation method and verify model input prices against official provider sources during implementation. Do not imply exact live usage or apply an RTK discount to static file totals.
27
+
28
+ ## V1 implementation
29
+
30
+ The authenticated `/api/token-estimate` endpoint requires exactly one supported `client` and `model`. It uses the latest scan snapshot, including a refreshed snapshot after Rescan. `/api/optimization-checks` runs bounded, fixed RTK identity probes on demand. Neither endpoint installs anything or contacts a model provider.
31
+
32
+ ### Estimation method
33
+
34
+ V1 uses `ceil(Unicode code points / 4)` per entry, whitespace-separated word counts and UTF-8 byte sizes. This lightweight, deterministic heuristic needs no network service or tokenizer dependency. It is approximate, particularly for code and non-Latin text, and is not interchangeable with any provider's tokenizer. Empty text has zero tokens. Totals sum the rounded entry estimates.
35
+
36
+ A skill catalog entry contains its name, a newline and its description. Its on-demand body excludes YAML frontmatter. Other skill metadata and client-generated catalog wrappers are not modeled. Identical skill copies are grouped; physical context-file aliases are counted once. References are followed locally, including references from rules and skills, with cycle protection. Link text stays in the referring file; linked contents stay on demand unless independently applicable at startup.
37
+
38
+ Startup means a session at the scanned project root. Root instructions, supported user instructions, unconditional rules and root/user skill catalogs qualify. Nested instructions and nested skill catalogs are on demand. Codex override files replace AGENTS.md in the same folder. Rule conditions use Cursor `alwaysApply`, Claude `paths`, and universal Copilot `applyTo` patterns. Other rules are on demand. Estimates inherit the scanner's file-size, directory-depth, symlink and discovery boundaries. Ancestor instructions outside the scanned root, remote sources, installed plugin stores and session-specific client settings are not inspected. The UI reports when user discovery was disabled and displays scan warnings.
39
+
40
+ ### Maintained pricing
41
+
42
+ Prices verified on 2026-10-10, USD per million standard uncached input tokens:
43
+
44
+ | Preset | Input price | Official source |
45
+ | ----------------- | ----------: | -------------------------------------------------------------------------- |
46
+ | GPT-5.3 Codex | $1.75 | [OpenAI API pricing](https://developers.openai.com/api/docs/pricing) |
47
+ | Claude Sonnet 5.5 | $2.00 | [Claude pricing](https://platform.claude.com/docs/en/about-claude/pricing) |
48
+ | Claude Opus 5.5 | $4.00 | [Claude pricing](https://platform.claude.com/docs/en/about-claude/pricing) |
49
+
50
+ Presets are pricing scenarios, not a promise that each client exposes each model. Maintainers update `src/token-estimator.ts` and this table together when prices change. Calculations exclude output tokens, caching, batch discounts, regional surcharges, subscription pricing and repeated session loads.
51
+
52
+ ### RTK detection
53
+
54
+ Detection requires both a valid `rtk --version` response and the `Rust Token Killer` identity in `rtk -h`, as declared in [RTK's CLI source](https://github.com/rtk-ai/rtk/blob/master/src/main.rs). Short help avoids opening or modifying an analytics database. Each process has a three-second timeout and a 64 KiB output limit. Relative PATH entries and paths inside the scanned project are excluded. Concurrent HTTP checks share one probe. Installation detection does not assert hook configuration or apply a discount to context totals.
55
+
56
+ The dedicated Optimization checklist tab loads an array of detected checks, initially RTK only. Token Optimizer does not run installation probes, and the checklist does not request token estimates. An unchecked disabled checkbox cannot be marked manually. Installation commands and the official guide are displayed when detection fails; Check again probes the server environment anew.
package/package.json CHANGED
@@ -1,6 +1,89 @@
1
1
  {
2
2
  "name": "@codeursenior/pek",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.7",
4
+ "description": "Agent Visualizer: explore your AI context, skills and MCP configuration in a local dashboard.",
5
+ "type": "module",
6
+ "bin": {
7
+ "pek": "dist/cli.js"
8
+ },
9
+ "files": [
10
+ "dist",
11
+ "README.md",
12
+ "LICENSE",
13
+ "docs"
14
+ ],
15
+ "engines": {
16
+ "node": ">=20"
17
+ },
18
+ "scripts": {
19
+ "build": "tsx scripts/clean.ts && npm run build:server && ng build",
20
+ "build:server": "tsc -p tsconfig.json",
21
+ "prestart": "npm run build",
22
+ "start": "node dist/cli.js audit agent",
23
+ "typecheck": "tsc -p tsconfig.json --noEmit && tsc -p tsconfig.test.json && ngc -p tsconfig.app.json --noEmit",
24
+ "lint": "eslint . && prettier --check .",
25
+ "test": "npm run test:server && npm run test:ui",
26
+ "test:server": "vitest run --config vitest.config.ts",
27
+ "test:ui": "ng test --watch=false",
28
+ "test:coverage": "npm run test:server -- --coverage && npm run test:ui -- --coverage",
29
+ "test:e2e": "playwright test",
30
+ "test:package": "tsx scripts/package-smoke.ts",
31
+ "test:reproducibility": "tsx scripts/reproducibility.ts",
32
+ "check": "npm run lint && npm run typecheck && npm run build && npm test",
33
+ "prepack": "npm run build",
34
+ "format": "prettier --write ."
35
+ },
36
+ "keywords": [
37
+ "agents",
38
+ "codex",
39
+ "copilot",
40
+ "claude",
41
+ "cursor",
42
+ "mcp",
43
+ "skills",
44
+ "visualizer"
45
+ ],
46
+ "author": "Simon Dieny",
47
+ "license": "MIT",
48
+ "repository": {
49
+ "type": "git",
50
+ "url": "git+https://github.com/codeursenior/product-engineer-kit.git",
51
+ "directory": "packages/agent-visualizer"
52
+ },
53
+ "homepage": "https://github.com/codeursenior/product-engineer-kit/tree/main/packages/agent-visualizer#readme",
54
+ "bugs": "https://github.com/codeursenior/product-engineer-kit/issues",
55
+ "publishConfig": {
56
+ "access": "public"
57
+ },
58
+ "dependencies": {
59
+ "jsonc-parser": "^3.3.1",
60
+ "smol-toml": "^1.4.2",
61
+ "yaml": "^2.8.1"
62
+ },
63
+ "devDependencies": {
64
+ "@angular/build": "22.2.0",
65
+ "@angular/cli": "22.2.0",
66
+ "@angular/common": "22.2.0",
67
+ "@angular/compiler": "22.2.0",
68
+ "@angular/compiler-cli": "22.2.0",
69
+ "@angular/core": "22.2.0",
70
+ "@angular/platform-browser": "22.2.0",
71
+ "@playwright/test": "1.63.0",
72
+ "@types/d3": "7.4.3",
73
+ "@types/node": "22.20.4",
74
+ "@types/node20": "npm:@types/node@20.19.43",
75
+ "@vitest/coverage-v8": "5.0.3",
76
+ "angular-eslint": "22.5.0",
77
+ "d3": "^7.9.0",
78
+ "eslint": "10.11.0",
79
+ "jiti": "2.7.0",
80
+ "jsdom": "30.1.1",
81
+ "prettier": "^3.9.8",
82
+ "rxjs": "7.8.2",
83
+ "tslib": "2.8.1",
84
+ "tsx": "4.23.15",
85
+ "typescript": "6.0.3",
86
+ "typescript-eslint": "8.71.0",
87
+ "vitest": "5.0.3"
88
+ }
89
+ }