tokentrace 0.19.2 → 0.20.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.
Files changed (40) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/README.md +1 -1
  3. package/TOKENTRACE_AGENT.md +11 -0
  4. package/app/api/evidence-pack/route.ts +3 -40
  5. package/app/guide/guide-content.ts +1 -1
  6. package/app/page.tsx +4 -4
  7. package/components/overview/summary-cards.tsx +64 -34
  8. package/components/sidebar.tsx +61 -25
  9. package/dist/cli/main.mjs +10 -0
  10. package/dist/runtime/agent.mjs +31 -1
  11. package/dist/runtime/chatgpt-app.mjs +3901 -0
  12. package/dist/runtime/mcp.mjs +31 -1
  13. package/docs/CHATGPT_APP_PROTOTYPE.md +103 -0
  14. package/docs/CHATGPT_APP_RELEASE.md +101 -0
  15. package/docs/chatgpt-app/README.md +24 -0
  16. package/docs/chatgpt-app/assets/icon.svg +10 -0
  17. package/docs/chatgpt-app/assets/listing-card.svg +34 -0
  18. package/docs/chatgpt-app/assets/widget-preview.svg +24 -0
  19. package/docs/chatgpt-app/dashboard-fields.json +39 -0
  20. package/docs/chatgpt-app/manual-release-steps.md +114 -0
  21. package/docs/chatgpt-app/privacy-and-data.md +39 -0
  22. package/docs/chatgpt-app/review-response-template.md +53 -0
  23. package/docs/chatgpt-app/screenshot-checklist.md +46 -0
  24. package/docs/chatgpt-app/submission-copy.md +45 -0
  25. package/docs/chatgpt-app/test-prompts-and-responses.md +61 -0
  26. package/llms.txt +13 -0
  27. package/next.config.mjs +8 -2
  28. package/package.json +11 -4
  29. package/scripts/build-cli-runtime.mjs +1 -0
  30. package/scripts/chatgpt-app-release-check.mjs +489 -0
  31. package/scripts/chatgpt-app.ts +107 -0
  32. package/scripts/package-inspect.mjs +6 -1
  33. package/scripts/smoke-packed-install.mjs +5 -0
  34. package/server.json +3 -3
  35. package/src/cli/commands.ts +8 -0
  36. package/src/cli/help.ts +3 -0
  37. package/src/lib/agent-discovery.ts +32 -1
  38. package/src/lib/chatgpt-app/prototype.ts +452 -0
  39. package/src/lib/chatgpt-app/server.ts +162 -0
  40. package/src/lib/evidence-pack.ts +47 -0
package/CHANGELOG.md CHANGED
@@ -4,6 +4,38 @@ All notable changes to TokenTrace are documented here.
4
4
 
5
5
  ## Unreleased
6
6
 
7
+ ## [0.20.0] - 2026-06-12
8
+
9
+ ### Changed
10
+
11
+ - **Overview and shell UI polish.** The dashboard shell now groups navigation by
12
+ task area, and the Overview summary surface makes local evidence,
13
+ cached/non-cache token states, and exact/estimated/unknown cost distinctions
14
+ easier to scan.
15
+ - **ChatGPT app feasibility documented.** Added a recommendation to prototype
16
+ only a private, redacted evidence-pack ChatGPT app workflow until public
17
+ distribution can preserve TokenTrace's local-first privacy boundary.
18
+ - **Private ChatGPT app prototype.** Added `tokentrace chatgpt-app` with a
19
+ local HTTP `/mcp` server, one read-only `get_redacted_evidence_pack` Apps SDK
20
+ tool, a compact MCP Apps widget resource, and a developer-mode runbook. The
21
+ prototype uses redacted evidence packs and does not scan files on startup or
22
+ publish a public app.
23
+ - **ChatGPT app release readiness.** Added `npm run release:chatgpt:check`, a
24
+ hosted `/mcp` validator, a release runbook, and optional tag-workflow wiring
25
+ so ChatGPT app readiness can be checked alongside npm and MCP registry
26
+ releases while keeping OpenAI Dashboard submission and publishing manual.
27
+ - **ChatGPT app submission kit.** Added `docs/chatgpt-app/` with step-by-step
28
+ personal-account Dashboard release instructions, ready-to-paste submission
29
+ copy, privacy and review templates, test prompts, screenshot guidance, and
30
+ reusable SVG app assets for the manual ChatGPT app submission flow.
31
+ - **Development dashboard CSP avoids React issue noise.** The local dev server
32
+ now allows React's development-only diagnostics without relaxing the
33
+ production Content Security Policy.
34
+ - **Product website moved to Baseframe Labs.** The package `homepage`, MCP
35
+ registry `websiteUrl`, README link, and in-app guide link now point to
36
+ `https://www.baseframelabs.com/apps/tokentrace`. The previous
37
+ `abhiyoheswaran.com` URL redirects there.
38
+
7
39
  ## [0.19.2] - 2026-06-05
8
40
 
9
41
  ### Changed
package/README.md CHANGED
@@ -8,7 +8,7 @@ Local-first AI CLI usage analytics. TokenTrace scans local CLI logs and local us
8
8
 
9
9
  TokenTrace is designed for local development machines first, with macOS-oriented defaults. It does not require a cloud account and does not send telemetry or logs anywhere.
10
10
 
11
- [Website](https://www.abhiyoheswaran.com/apps/tokentrace) · [Source](https://github.com/abhiyoheswaran1/tokentrace)
11
+ [Website](https://www.baseframelabs.com/apps/tokentrace) · [Source](https://github.com/abhiyoheswaran1/tokentrace)
12
12
 
13
13
  ![TokenTrace overview dashboard](docs/assets/overview-0.12.0.png)
14
14
 
@@ -44,6 +44,17 @@ The MCP server does not scan on startup. Its `run_scan` tool requires
44
44
  `confirmLocalScan=true` before reading local usage files or writing the local
45
45
  database.
46
46
 
47
+ Private ChatGPT developer-mode prototype:
48
+
49
+ ```bash
50
+ tokentrace chatgpt-app selftest --json
51
+ tokentrace chatgpt-app --port 8787 --hostname 127.0.0.1
52
+ ```
53
+
54
+ This starts a local HTTP `/mcp` server for one read-only redacted evidence-pack
55
+ tool. ChatGPT requires an HTTPS tunnel for developer-mode connector testing.
56
+ See `docs/CHATGPT_APP_PROTOTYPE.md`.
57
+
47
58
  If the local dashboard is already running, the same manifest is available from:
48
59
 
49
60
  ```bash
@@ -1,7 +1,6 @@
1
1
  import { NextResponse } from "next/server";
2
- import { getAnalyticsData } from "@/src/lib/analytics";
3
- import { buildEvidencePack, renderEvidencePackMarkdown } from "@/src/lib/evidence-pack";
4
- import { buildEvidenceTrail, parseEvidenceMetric } from "@/src/lib/evidence-trail";
2
+ import { buildMetricEvidencePack, renderEvidencePackMarkdown } from "@/src/lib/evidence-pack";
3
+ import { parseEvidenceMetric } from "@/src/lib/evidence-trail";
5
4
 
6
5
  export const dynamic = "force-dynamic";
7
6
 
@@ -9,43 +8,7 @@ export async function GET(request: Request) {
9
8
  const url = new URL(request.url);
10
9
  const format = url.searchParams.get("format") === "markdown" ? "markdown" : "json";
11
10
  const metric = parseEvidenceMetric(url.searchParams.get("metric"));
12
- const trail = buildEvidenceTrail({ metric });
13
- const analytics = getAnalyticsData();
14
- const pack = buildEvidencePack({
15
- scope: {
16
- type: "metric",
17
- id: metric,
18
- label: trail.title
19
- },
20
- totals: trail.totals,
21
- confidenceDrivers: [
22
- `${trail.confidence.exact.toLocaleString()} exact interactions`,
23
- `${trail.confidence.estimated.toLocaleString()} estimated interactions`,
24
- `${trail.confidence.unknown.toLocaleString()} unknown interactions`,
25
- `Data confidence ${analytics.dataConfidence.score}/100`
26
- ],
27
- sourceFiles: trail.sourceFiles.map((source) => source.sourceFile),
28
- parserNotes: trail.sessions
29
- .map((session) => `${session.parser ?? "unknown parser"}: ${session.parserStatus ?? "unknown status"}`)
30
- .slice(0, 20),
31
- modelRateState: trail.sessions
32
- .map((session) =>
33
- session.pricingHref ? `${session.model}: model-rate link available` : `${session.model}: no model-rate link`
34
- )
35
- .slice(0, 20),
36
- repairLinks: trail.sessions
37
- .filter((session) => session.unknownCostInteractions > 0)
38
- .map((session) => `/repair?source=${encodeURIComponent(session.sourceFile)}`),
39
- records: trail.sessions.map((session) => ({
40
- id: session.id,
41
- role: "session",
42
- model: session.model,
43
- sourceFile: session.sourceFile,
44
- totalTokens: session.totalTokens,
45
- cost: session.cost,
46
- interactions: session.interactions
47
- }))
48
- });
11
+ const pack = buildMetricEvidencePack({ metric });
49
12
 
50
13
  if (format === "markdown") {
51
14
  return new NextResponse(renderEvidencePackMarkdown(pack), {
@@ -1,6 +1,6 @@
1
1
  import { ClipboardList, Gauge, Search, Wrench } from "lucide-react";
2
2
 
3
- export const PRODUCT_WEBSITE_URL = "https://www.abhiyoheswaran.com/apps/tokentrace";
3
+ export const PRODUCT_WEBSITE_URL = "https://www.baseframelabs.com/apps/tokentrace";
4
4
 
5
5
  export const guideNav = [
6
6
  ["#start", "Start here", "First scan to first evidence"],
package/app/page.tsx CHANGED
@@ -53,11 +53,11 @@ async function OverviewPrimarySection({ range }: { range: ResolvedDateRange }) {
53
53
  .map((entry) => ({ date: entry.date, value: entry.value, severity: entry.severity }));
54
54
 
55
55
  return (
56
- <div className="space-y-8">
56
+ <div className="overview-primary-section space-y-7">
57
57
  {summary.interactions === 0 ? <FirstRunPanel status={firstRunStatus} /> : null}
58
58
  <UsagePulsePanel comparison={data.comparison} />
59
59
  <DataConfidenceStrip confidence={data.dataConfidence} />
60
- <div className="grid gap-3 md:grid-cols-2 xl:grid-cols-6">
60
+ <div className="overview-summary-grid grid gap-3 md:grid-cols-2 xl:grid-cols-6">
61
61
  <TokenAccountingCard
62
62
  summary={summary}
63
63
  processedHref={evidenceLinks["processed-tokens"]}
@@ -109,7 +109,7 @@ async function OverviewRepairSection({ range }: { range: ResolvedDateRange }) {
109
109
  }
110
110
 
111
111
  return (
112
- <div className="space-y-8">
112
+ <div className="overview-repair-section space-y-7">
113
113
  {summary.interactions > 0 ? (
114
114
  <OverviewReviewStatusStrip
115
115
  report={doctorReport}
@@ -140,7 +140,7 @@ export default async function OverviewPage({ searchParams }: OverviewPageProps)
140
140
  const range = resolveDateRange(params);
141
141
 
142
142
  return (
143
- <div className="space-y-8">
143
+ <div className="overview-workbench space-y-7">
144
144
  <PageHeader
145
145
  title="Overview"
146
146
  description="Local token, cost, model, and session analytics across AI CLI tools."
@@ -6,6 +6,34 @@ import { DataValue, FieldLabel } from "@/components/ui/typography";
6
6
  import { formatCurrency, formatTokens } from "@/src/lib/format";
7
7
  import { cn } from "@/src/lib/utils";
8
8
 
9
+ type DetailItem = {
10
+ label: string;
11
+ value: string;
12
+ };
13
+
14
+ function EvidenceAction({
15
+ href,
16
+ label,
17
+ muted = false
18
+ }: {
19
+ href: string;
20
+ label: string;
21
+ muted?: boolean;
22
+ }) {
23
+ return (
24
+ <Link
25
+ href={href}
26
+ className={cn(
27
+ "inline-flex w-fit items-center gap-1.5 text-xs font-medium underline-offset-4 hover:underline",
28
+ muted ? "text-muted-foreground hover:text-foreground" : "text-primary"
29
+ )}
30
+ >
31
+ <span>{label}</span>
32
+ <ArrowRight className="h-3.5 w-3.5" aria-hidden="true" />
33
+ </Link>
34
+ );
35
+ }
36
+
9
37
  function CostSessionsMetricPane({
10
38
  label,
11
39
  value,
@@ -22,7 +50,7 @@ function CostSessionsMetricPane({
22
50
  }: {
23
51
  label: string;
24
52
  value: string;
25
- detailItems: string[];
53
+ detailItems: DetailItem[];
26
54
  href: string;
27
55
  actionLabel: string;
28
56
  secondaryHref?: string;
@@ -36,7 +64,7 @@ function CostSessionsMetricPane({
36
64
  return (
37
65
  <section
38
66
  className={cn(
39
- "cost-sessions-section grid min-w-0 grid-rows-[auto_auto_auto_auto_1fr_auto] p-4 md:row-span-6 md:grid-rows-subgrid",
67
+ "cost-sessions-section cost-sessions-metric-pane grid min-w-0 grid-rows-[auto_auto_auto_auto_1fr_auto] p-4 md:row-span-6 md:grid-rows-subgrid",
40
68
  className
41
69
  )}
42
70
  >
@@ -53,8 +81,9 @@ function CostSessionsMetricPane({
53
81
 
54
82
  <div className="mt-3 flex flex-wrap items-start gap-x-2 gap-y-1 text-xs leading-snug text-muted-foreground">
55
83
  {detailItems.map((item, index) => (
56
- <span key={item} className="inline-flex min-w-0 items-center gap-2">
57
- <span className="min-w-0">{item}</span>
84
+ <span key={item.label} className="inline-flex min-w-0 items-center gap-1.5">
85
+ <span className="font-medium text-foreground">{item.label}</span>
86
+ <span className="min-w-0">{item.value}</span>
58
87
  {index < detailItems.length - 1 ? <span className="hidden text-border sm:inline">/</span> : null}
59
88
  </span>
60
89
  ))}
@@ -67,16 +96,14 @@ function CostSessionsMetricPane({
67
96
 
68
97
  <div aria-hidden="true" />
69
98
 
70
- <div className="flex flex-wrap items-center gap-x-3 gap-y-1 pt-4">
71
- <Link href={href} className="inline-flex w-fit items-center gap-1.5 text-xs font-medium text-primary underline-offset-4 hover:underline">
72
- {actionLabel}
73
- <ArrowRight className="h-3.5 w-3.5" aria-hidden="true" />
74
- </Link>
75
- {secondaryHref ? (
76
- <Link href={secondaryHref} className="inline-flex w-fit items-center gap-1.5 text-xs font-medium text-muted-foreground underline-offset-4 hover:text-foreground hover:underline">
77
- {secondaryActionLabel ?? "View details"}
78
- </Link>
79
- ) : null}
99
+ <div className="pt-4">
100
+ <FieldLabel className="mb-2">Local evidence</FieldLabel>
101
+ <div className="flex flex-wrap items-center gap-x-3 gap-y-1">
102
+ <EvidenceAction href={href} label={actionLabel} />
103
+ {secondaryHref ? (
104
+ <EvidenceAction href={secondaryHref} label={secondaryActionLabel ?? "View details"} muted />
105
+ ) : null}
106
+ </div>
80
107
  </div>
81
108
  </section>
82
109
  );
@@ -124,9 +151,9 @@ export function CostSessionsCard({
124
151
  value={formatCurrency(summary.totalCost)}
125
152
  valueClassName="wrap-break-word text-2xl"
126
153
  detailItems={[
127
- `${formatCurrency(summary.exactCost)} exact`,
128
- `${formatCurrency(summary.estimatedCost)} estimated`,
129
- `${summary.unknownCostInteractions.toLocaleString()} unknown`
154
+ { label: "Exact", value: formatCurrency(summary.exactCost) },
155
+ { label: "Estimated", value: formatCurrency(summary.estimatedCost) },
156
+ { label: "Unknown", value: summary.unknownCostInteractions.toLocaleString() }
130
157
  ]}
131
158
  href={costHref}
132
159
  actionLabel={costActionLabel}
@@ -140,7 +167,7 @@ export function CostSessionsCard({
140
167
  className="border-t md:border-l md:border-t-0"
141
168
  label="Sessions"
142
169
  value={summary.sessions.toLocaleString()}
143
- detailItems={[`${summary.interactions.toLocaleString()} interactions`]}
170
+ detailItems={[{ label: "Interactions", value: summary.interactions.toLocaleString() }]}
144
171
  href={sessionsHref}
145
172
  actionLabel="View evidence"
146
173
  trustLabel="Session trust"
@@ -195,8 +222,8 @@ export function TokenAccountingCard({
195
222
  }) {
196
223
  const evidenceActions = [
197
224
  { label: "Processed", href: processedHref },
198
- { label: "Fresh", href: freshHref },
199
- { label: "Cache", href: cacheHref }
225
+ { label: "Fresh / non-cache", href: freshHref },
226
+ { label: "Cache read/write", href: cacheHref }
200
227
  ];
201
228
 
202
229
  return (
@@ -248,7 +275,7 @@ export function TokenAccountingCard({
248
275
  detail={`${formatTokens(summary.inputTokens)} input / ${formatTokens(summary.outputTokens)} output / ${formatTokens(summary.reasoningTokens)} reasoning`}
249
276
  />
250
277
  <TokenAccountingSlice
251
- label="Cache"
278
+ label="Cache read/write"
252
279
  value={formatTokens(summary.cachedTokens)}
253
280
  detail={`${formatTokens(summary.cacheReadTokens)} read / ${formatTokens(summary.cacheWriteTokens)} write`}
254
281
  />
@@ -261,19 +288,22 @@ export function TokenAccountingCard({
261
288
  </span>
262
289
  </p>
263
290
 
264
- <div className="mt-auto flex flex-wrap items-center gap-x-3 gap-y-1 pt-4">
265
- {evidenceActions.map((action) => (
266
- <Link
267
- key={action.href}
268
- href={action.href}
269
- aria-label={`View ${action.label.toLowerCase()} token evidence`}
270
- className="inline-flex w-fit items-center gap-1.5 text-xs font-medium text-primary underline-offset-4 hover:underline"
271
- >
272
- <span className="text-muted-foreground">{action.label}</span>
273
- <span>View evidence</span>
274
- <ArrowRight className="h-3.5 w-3.5" aria-hidden="true" />
275
- </Link>
276
- ))}
291
+ <div className="mt-auto pt-4">
292
+ <FieldLabel className="mb-2">Local evidence</FieldLabel>
293
+ <div className="flex flex-wrap items-center gap-x-3 gap-y-1">
294
+ {evidenceActions.map((action) => (
295
+ <Link
296
+ key={action.href}
297
+ href={action.href}
298
+ aria-label={`View ${action.label.toLowerCase()} token evidence`}
299
+ className="inline-flex w-fit items-center gap-1.5 text-xs font-medium text-primary underline-offset-4 hover:underline"
300
+ >
301
+ <span className="text-muted-foreground">{action.label}</span>
302
+ <span>View evidence</span>
303
+ <ArrowRight className="h-3.5 w-3.5" aria-hidden="true" />
304
+ </Link>
305
+ ))}
306
+ </div>
277
307
  </div>
278
308
  </CardContent>
279
309
  </Card>
@@ -26,28 +26,52 @@ import { formatAppVersion, getAppVersion } from "@/src/lib/app-version";
26
26
  import { cn } from "@/src/lib/utils";
27
27
 
28
28
  const overviewNavItem = { href: "/", label: "Overview", icon: Gauge };
29
+ const toolsNavItem = { href: "/tools", label: "Tools", icon: Terminal };
30
+ const modelsNavItem = { href: "/models", label: "Models", icon: Bot };
31
+ const projectsNavItem = { href: "/projects", label: "Projects", icon: FolderGit2 };
32
+ const sessionsNavItem = { href: "/sessions", label: "Sessions", icon: Search };
33
+ const insightsNavItem = { href: "/optimisation", label: "Insights", icon: Sparkles };
34
+ const queryNavItem = { href: "/query", label: "Query", icon: Database };
35
+ const repairNavItem = { href: "/repair", label: "Repair", icon: Wrench };
36
+ const pricingNavItem = { href: "/pricing", label: "Model Rates", icon: SlidersHorizontal };
37
+ const diagnosticsNavItem = { href: "/diagnostics", label: "Scan Health", icon: ClipboardList };
38
+ const discoveryNavItem = { href: "/discovery", label: "Discovery", icon: BarChart3 };
39
+ const parsersNavItem = { href: "/parser-debug", label: "Parsers", icon: Bug };
40
+ const rawDataNavItem = { href: "/debug", label: "Raw Data", icon: Bug };
41
+ const settingsNavItem = { href: "/settings", label: "Settings", icon: Settings };
29
42
 
30
- const primaryNavItems = [
31
- overviewNavItem,
32
- { href: "/tools", label: "Tools", icon: Terminal },
33
- { href: "/models", label: "Models", icon: Bot },
34
- { href: "/projects", label: "Projects", icon: FolderGit2 },
35
- { href: "/sessions", label: "Sessions", icon: Search },
36
- { href: "/optimisation", label: "Insights", icon: Sparkles },
37
- { href: "/query", label: "Query", icon: Database },
38
- { href: "/repair", label: "Repair", icon: Wrench },
39
- { href: "/pricing", label: "Model Rates", icon: SlidersHorizontal },
40
- { href: "/diagnostics", label: "Scan Health", icon: ClipboardList },
41
- { href: "/discovery", label: "Discovery", icon: BarChart3 },
42
- { href: "/parser-debug", label: "Parsers", icon: Bug },
43
- { href: "/debug", label: "Raw Data", icon: Bug },
44
- { href: "/settings", label: "Settings", icon: Settings }
43
+ const navSections = [
44
+ {
45
+ label: "Analyze",
46
+ items: [overviewNavItem, toolsNavItem, modelsNavItem, projectsNavItem]
47
+ },
48
+ {
49
+ label: "Investigate",
50
+ items: [sessionsNavItem, insightsNavItem, queryNavItem, repairNavItem]
51
+ },
52
+ {
53
+ label: "Maintain",
54
+ items: [
55
+ pricingNavItem,
56
+ diagnosticsNavItem,
57
+ discoveryNavItem,
58
+ parsersNavItem,
59
+ rawDataNavItem,
60
+ settingsNavItem
61
+ ]
62
+ }
45
63
  ];
46
64
 
47
65
  const supportNavItems = [
48
66
  { href: "/guide", label: "Guide", icon: BookOpen }
49
67
  ];
50
68
 
69
+ const primaryNavItems = navSections.flatMap((section) => section.items);
70
+ const sidebarSections = [
71
+ ...navSections.map((section) => ({ ...section, variant: "default" as const })),
72
+ { label: "Reference", items: supportNavItems, variant: "support" as const }
73
+ ];
74
+
51
75
  const priorityMobileItems = primaryNavItems.filter((item) =>
52
76
  ["/", "/sessions", "/repair", "/diagnostics", "/settings"].includes(item.href)
53
77
  );
@@ -76,7 +100,7 @@ function NavLink({
76
100
  : cn(
77
101
  "flex items-center gap-3 rounded-md px-3 py-2 text-sm transition-colors",
78
102
  isActive
79
- ? "bg-primary/10 font-medium text-primary"
103
+ ? "bg-muted font-medium text-primary shadow-xs ring-1 ring-border/60"
80
104
  : variant === "support"
81
105
  ? "border bg-muted/40 font-medium text-foreground hover:bg-muted"
82
106
  : "text-muted-foreground hover:bg-muted hover:text-foreground"
@@ -140,17 +164,29 @@ export function Sidebar({ appVersion = getAppVersion() }: { appVersion?: string
140
164
  </div>
141
165
  </div>
142
166
  </div>
143
- <nav aria-label="Primary navigation" className="flex-1 space-y-1 overflow-y-auto p-3">
144
- {primaryNavItems.map((item) => (
145
- <NavLink key={item.href} item={item} isActive={isActiveRoute(pathname, item.href)} />
146
- ))}
147
- </nav>
148
- <nav aria-label="Help navigation" className="p-3">
149
- {supportNavItems.map((item) => (
150
- <NavLink key={item.href} item={item} variant="support" isActive={isActiveRoute(pathname, item.href)} />
151
- ))}
167
+ <nav aria-label="Primary navigation" className="flex-1 overflow-y-auto p-3">
168
+ <div className="space-y-4">
169
+ {sidebarSections.map((section) => (
170
+ <section key={section.label} aria-label={`${section.label} navigation`} className="space-y-1">
171
+ <div className="px-3 pb-1 text-[0.68rem] font-semibold uppercase leading-none tracking-normal text-muted-foreground">
172
+ {section.label}
173
+ </div>
174
+ {section.items.map((item) => (
175
+ <NavLink
176
+ key={item.href}
177
+ item={item}
178
+ variant={section.variant}
179
+ isActive={isActiveRoute(pathname, item.href)}
180
+ />
181
+ ))}
182
+ </section>
183
+ ))}
184
+ </div>
152
185
  </nav>
153
186
  <div className="border-t p-4 text-xs text-muted-foreground">
187
+ <div className="mb-2 text-[0.68rem] font-semibold uppercase leading-none tracking-normal text-muted-foreground">
188
+ Local processing
189
+ </div>
154
190
  <div className="flex flex-wrap items-center gap-x-2 gap-y-1 leading-relaxed">
155
191
  <span className="font-medium text-foreground">{formatAppVersion(appVersion)}</span>
156
192
  <span aria-hidden="true">·</span>
package/dist/cli/main.mjs CHANGED
@@ -82,6 +82,9 @@ Usage:
82
82
  tokentrace mcp Start the local stdio MCP server
83
83
  tokentrace mcp selftest --json
84
84
  Verify the local MCP entrypoint without scanning files
85
+ tokentrace chatgpt-app Start the private ChatGPT app prototype
86
+ tokentrace chatgpt-app selftest --json
87
+ Verify the ChatGPT app prototype without scanning files
85
88
  tokentrace scan Scan local AI CLI usage logs
86
89
  tokentrace doctor --json
87
90
  Inspect scan health and repair recommendations
@@ -458,6 +461,9 @@ async function mcp(context, args) {
458
461
  }
459
462
  await runNodeScript(context, "mcp", [], { env: process.env });
460
463
  }
464
+ async function chatGptApp(context, args) {
465
+ await runNodeScript(context, "chatgpt-app", args, { env: process.env });
466
+ }
461
467
  async function doctor(context, args) {
462
468
  if (await printScriptHelp(context, "doctor", args)) return;
463
469
  await initializeDatabase(context, { quiet: true, refreshPrices: false });
@@ -651,6 +657,10 @@ async function runCliCommand(context, rawArgs = process.argv.slice(2)) {
651
657
  await mcp(context, args);
652
658
  return;
653
659
  }
660
+ if (command === "chatgpt-app") {
661
+ await chatGptApp(context, args);
662
+ return;
663
+ }
654
664
  if (command === "scan") {
655
665
  await scan(context, args);
656
666
  return;
@@ -4006,7 +4006,7 @@ var product = {
4006
4006
  name: "TokenTrace",
4007
4007
  packageName: "tokentrace",
4008
4008
  description: "Local-first dashboard and CLI for AI coding-agent token, cost, session, and parser analytics.",
4009
- homepage: "https://www.abhiyoheswaran.com/apps/tokentrace",
4009
+ homepage: "https://www.baseframelabs.com/apps/tokentrace",
4010
4010
  repository: "https://github.com/abhiyoheswaran1/tokentrace"
4011
4011
  };
4012
4012
  var commands = [
@@ -4143,6 +4143,27 @@ var commands = [
4143
4143
  "The run_scan MCP tool requires confirmLocalScan=true before reading local usage files and writing the local database."
4144
4144
  ]
4145
4145
  },
4146
+ {
4147
+ id: "chatgpt-app",
4148
+ title: "Start private ChatGPT app prototype",
4149
+ command: ["tokentrace", "chatgpt-app"],
4150
+ description: "Start a local HTTP MCP Apps server for ChatGPT developer-mode testing. It exposes one read-only redacted evidence-pack tool and widget.",
4151
+ output: "terminal",
4152
+ mutatesLocalState: false,
4153
+ startsLongRunningProcess: true,
4154
+ requiresNetwork: false,
4155
+ safeForAutomation: false,
4156
+ useWhen: "The human wants to test TokenTrace as a private ChatGPT connector without publishing a public app.",
4157
+ followUps: [
4158
+ ["tokentrace", "chatgpt-app", "selftest", "--json"],
4159
+ ["tokentrace", "chatgpt-app", "--port", "8787", "--hostname", "127.0.0.1"]
4160
+ ],
4161
+ notes: [
4162
+ "ChatGPT developer mode requires exposing the local /mcp endpoint through an HTTPS tunnel.",
4163
+ "The prototype does not scan files on startup and returns only redacted evidence packs.",
4164
+ "See docs/CHATGPT_APP_PROTOTYPE.md for connector setup steps."
4165
+ ]
4166
+ },
4146
4167
  {
4147
4168
  id: "status",
4148
4169
  title: "Print local live usage status",
@@ -4373,6 +4394,15 @@ function buildAgentDiscoveryManifest(options = {}) {
4373
4394
  steps: [
4374
4395
  ["tokentrace", "watch", "--session", "--compact"]
4375
4396
  ]
4397
+ },
4398
+ {
4399
+ id: "chatgpt-app-prototype",
4400
+ title: "Private ChatGPT app prototype",
4401
+ goal: "Start the local Apps SDK prototype and expose a redacted evidence-pack connector for developer-mode testing.",
4402
+ steps: [
4403
+ ["tokentrace", "chatgpt-app", "selftest", "--json"],
4404
+ ["tokentrace", "chatgpt-app", "--port", "8787", "--hostname", "127.0.0.1"]
4405
+ ]
4376
4406
  }
4377
4407
  ],
4378
4408
  integrations: {