@akta/dao-cli 0.1.0 → 0.1.1

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 CHANGED
@@ -130,7 +130,7 @@ bun install
130
130
  bun run src/index.ts
131
131
  ```
132
132
 
133
- Requires [akita-sdk](https://github.com/akita-protocol/akita-sc) built locally at `../../akita-sc/projects/akita-sdk`.
133
+ Uses [`@akta/sdk`](https://www.npmjs.com/package/@akta/sdk) from npm installed automatically with `bun install`.
134
134
 
135
135
  ## License
136
136
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@akta/dao-cli",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Read-only CLI and TUI for querying Akita DAO state on Algorand",
5
5
  "type": "module",
6
6
  "bin": {
package/src/tui/app.ts CHANGED
@@ -157,9 +157,10 @@ export async function startTUI(network: AkitaNetwork): Promise<void> {
157
157
  fixedRight = result.fixedRight;
158
158
  }
159
159
 
160
- // JSON mode: re-render as JSON (flatten both panels)
160
+ // JSON mode: serialize structured data (or fall back to lines)
161
161
  if (state.jsonMode) {
162
- const jsonStr = JSON.stringify(lines, jsonReplacer, 2);
162
+ const jsonData = (result as LoadResult).data ?? lines;
163
+ const jsonStr = JSON.stringify(jsonData, jsonReplacer, 2);
163
164
  lines = jsonStr.split("\n");
164
165
  fixedRight = undefined;
165
166
  }
package/src/tui/types.ts CHANGED
@@ -65,6 +65,7 @@ export interface ViewContext {
65
65
  export interface LoadResult {
66
66
  lines: string[];
67
67
  fixedRight?: string[];
68
+ data?: unknown;
68
69
  }
69
70
 
70
71
  export interface View {
@@ -15,10 +15,10 @@ import {
15
15
  colorState,
16
16
  } from "../../formatting";
17
17
  import { renderPanel, renderPanelGrid, splitWidth } from "../panels";
18
- import type { View, ViewContext } from "../types";
18
+ import type { LoadResult, View, ViewContext } from "../types";
19
19
 
20
20
  export const daoView: View = {
21
- async load(ctx: ViewContext): Promise<string[]> {
21
+ async load(ctx: ViewContext): Promise<LoadResult> {
22
22
  const { dao, network, width } = ctx;
23
23
  const state = await dao.getGlobalState();
24
24
  const ids = getNetworkAppIds(network);
@@ -53,75 +53,81 @@ export const daoView: View = {
53
53
  // Asset info fetch failed — skip supply charts
54
54
  }
55
55
 
56
- if (width < 80) {
57
- return renderSingleColumn(state, network, ids, width, aktaSupply, bonesSupply);
58
- }
56
+ // Build structured data for JSON mode
57
+ const data = buildDaoData(state, network, ids, dao.appId, aktaSupply, bonesSupply);
59
58
 
60
- const gridRows: string[][][] = [];
61
-
62
- // Row 1: DAO info + Assets + Token Supply
63
- const hasSupply = aktaSupply || bonesSupply;
64
- const colCount = hasSupply ? 3 : 2;
65
- const colWidths = splitWidth(width, colCount);
66
-
67
- const infoContent = renderKV([
68
- ["Network", network],
69
- ["App ID", dao.appId.toString()],
70
- ["Version", state.version ?? "-"],
71
- ["State", state.state !== undefined ? colorState(daoStateLabel(state.state)) : "-"],
72
- ["Wallet", state.wallet ? resolveAppName(state.wallet, network) : "-"],
73
- ]);
74
- const infoPanel = renderPanel(infoContent, { title: "Akita DAO", width: colWidths[0] });
75
-
76
- const assetsContent = renderKV([
77
- ["AKTA", state.akitaAssets?.akta?.toString() ?? ids.akta.toString()],
78
- ["BONES", state.akitaAssets?.bones?.toString() ?? ids.bones.toString()],
79
- ["Next Proposal", state.proposalId?.toString() ?? "-"],
80
- ["Action Limit", state.proposalActionLimit?.toString() ?? "-"],
81
- ["Min Rewards", state.minRewardsImpact?.toString() ?? "-"],
82
- ]);
83
- const assetsPanel = renderPanel(assetsContent, { title: "Assets", width: colWidths[1] });
84
-
85
- if (hasSupply) {
86
- const supplyW = colWidths[2];
87
- const supplyContent = renderSupplyCharts(aktaSupply, bonesSupply, supplyW - 4);
88
- const supplyPanel = renderPanel(supplyContent, { title: "Token Supply", width: supplyW });
89
- gridRows.push([infoPanel, assetsPanel, supplyPanel]);
59
+ let lines: string[];
60
+ if (width < 80) {
61
+ lines = renderSingleColumn(state, network, ids, width, aktaSupply, bonesSupply);
90
62
  } else {
91
- gridRows.push([infoPanel, assetsPanel]);
92
- }
93
-
94
- // Row 3: App IDs (40%) + Proposal Settings / Revenue Splits stacked (60%)
95
- const appLines = renderAppTable(state, network);
96
- const proposalLines = renderProposalSettings(state);
97
-
98
- if (appLines.length > 0 || proposalLines.length > 0) {
99
- // ~40/60 split
100
- const appW = Math.floor((width - 2) * 0.4);
101
- const rightPanelW = width - appW - 2;
102
- const revLines = renderRevenueSplits(state, network, rightPanelW - 4);
103
-
104
- const leftPanel = appLines.length > 0
105
- ? renderPanel(appLines, { title: "App IDs", width: appW })
106
- : renderPanel([" No app ID data"], { title: "App IDs", width: appW });
107
-
108
- // Stack Proposal Settings + Revenue Splits into one right column
109
- const rightPanels: string[] = [];
110
- if (proposalLines.length > 0) {
111
- rightPanels.push(...renderPanel(proposalLines, { title: "Proposal Settings", width: rightPanelW }));
112
- }
113
- if (revLines.length > 0) {
114
- if (rightPanels.length > 0) rightPanels.push("");
115
- rightPanels.push(...renderPanel(revLines, { title: "Revenue Splits", width: rightPanelW }));
63
+ const gridRows: string[][][] = [];
64
+
65
+ // Row 1: DAO info + Assets + Token Supply
66
+ const hasSupply = aktaSupply || bonesSupply;
67
+ const colCount = hasSupply ? 3 : 2;
68
+ const colWidths = splitWidth(width, colCount);
69
+
70
+ const infoContent = renderKV([
71
+ ["Network", network],
72
+ ["App ID", dao.appId.toString()],
73
+ ["Version", state.version ?? "-"],
74
+ ["State", state.state !== undefined ? colorState(daoStateLabel(state.state)) : "-"],
75
+ ["Wallet", state.wallet ? resolveAppName(state.wallet, network) : "-"],
76
+ ]);
77
+ const infoPanel = renderPanel(infoContent, { title: "Akita DAO", width: colWidths[0] });
78
+
79
+ const assetsContent = renderKV([
80
+ ["AKTA", state.akitaAssets?.akta?.toString() ?? ids.akta.toString()],
81
+ ["BONES", state.akitaAssets?.bones?.toString() ?? ids.bones.toString()],
82
+ ["Next Proposal", state.proposalId?.toString() ?? "-"],
83
+ ["Action Limit", state.proposalActionLimit?.toString() ?? "-"],
84
+ ["Min Rewards", state.minRewardsImpact?.toString() ?? "-"],
85
+ ]);
86
+ const assetsPanel = renderPanel(assetsContent, { title: "Assets", width: colWidths[1] });
87
+
88
+ if (hasSupply) {
89
+ const supplyW = colWidths[2];
90
+ const supplyContent = renderSupplyCharts(aktaSupply, bonesSupply, supplyW - 4);
91
+ const supplyPanel = renderPanel(supplyContent, { title: "Token Supply", width: supplyW });
92
+ gridRows.push([infoPanel, assetsPanel, supplyPanel]);
93
+ } else {
94
+ gridRows.push([infoPanel, assetsPanel]);
116
95
  }
117
- if (rightPanels.length === 0) {
118
- rightPanels.push(...renderPanel([" No proposal settings"], { title: "Proposal Settings", width: rightPanelW }));
96
+
97
+ // Row 3: App IDs (40%) + Proposal Settings / Revenue Splits stacked (60%)
98
+ const appLines = renderAppTable(state, network);
99
+ const proposalLines = renderProposalSettings(state);
100
+
101
+ if (appLines.length > 0 || proposalLines.length > 0) {
102
+ // ~40/60 split
103
+ const appW = Math.floor((width - 2) * 0.4);
104
+ const rightPanelW = width - appW - 2;
105
+ const revLines = renderRevenueSplits(state, network, rightPanelW - 4);
106
+
107
+ const leftPanel = appLines.length > 0
108
+ ? renderPanel(appLines, { title: "App IDs", width: appW })
109
+ : renderPanel([" No app ID data"], { title: "App IDs", width: appW });
110
+
111
+ // Stack Proposal Settings + Revenue Splits into one right column
112
+ const rightPanels: string[] = [];
113
+ if (proposalLines.length > 0) {
114
+ rightPanels.push(...renderPanel(proposalLines, { title: "Proposal Settings", width: rightPanelW }));
115
+ }
116
+ if (revLines.length > 0) {
117
+ if (rightPanels.length > 0) rightPanels.push("");
118
+ rightPanels.push(...renderPanel(revLines, { title: "Revenue Splits", width: rightPanelW }));
119
+ }
120
+ if (rightPanels.length === 0) {
121
+ rightPanels.push(...renderPanel([" No proposal settings"], { title: "Proposal Settings", width: rightPanelW }));
122
+ }
123
+
124
+ gridRows.push([leftPanel, rightPanels]);
119
125
  }
120
126
 
121
- gridRows.push([leftPanel, rightPanels]);
127
+ lines = ["", ...renderPanelGrid(gridRows, { rowGap: 1 })];
122
128
  }
123
129
 
124
- return ["", ...renderPanelGrid(gridRows, { rowGap: 1 })];
130
+ return { lines, data };
125
131
  },
126
132
  };
127
133
 
@@ -176,6 +182,83 @@ function renderSingleColumn(
176
182
  return lines;
177
183
  }
178
184
 
185
+ // ── Structured data for JSON mode ───────────────────────────────
186
+
187
+ function buildDaoData(
188
+ state: Partial<AkitaDaoGlobalState>,
189
+ network: AkitaNetwork,
190
+ ids: { akta: bigint; bones: bigint },
191
+ appId: bigint,
192
+ aktaSupply: SupplyInfo | null,
193
+ bonesSupply: SupplyInfo | null,
194
+ ) {
195
+ // App ID lists
196
+ const appSections: [string, Record<string, bigint> | undefined][] = [
197
+ ["core", state.akitaAppList as Record<string, bigint> | undefined],
198
+ ["social", state.akitaSocialAppList as Record<string, bigint> | undefined],
199
+ ["plugins", state.pluginAppList as Record<string, bigint> | undefined],
200
+ ["other", state.otherAppList as Record<string, bigint> | undefined],
201
+ ];
202
+ const apps: Record<string, Record<string, bigint>> = {};
203
+ for (const [category, list] of appSections) {
204
+ if (list) apps[category] = list;
205
+ }
206
+
207
+ // Proposal settings
208
+ const psEntries: [string, { fee: bigint; power: bigint; duration: bigint; participation: bigint; approval: bigint } | undefined][] = [
209
+ ["upgradeApp", state.upgradeAppProposalSettings],
210
+ ["addPlugin", state.addPluginProposalSettings],
211
+ ["removePlugin", state.removePluginProposalSettings],
212
+ ["removeExecutePlugin", state.removeExecutePluginProposalSettings],
213
+ ["addAllowances", state.addAllowancesProposalSettings],
214
+ ["removeAllowances", state.removeAllowancesProposalSettings],
215
+ ["newEscrow", state.newEscrowProposalSettings],
216
+ ["toggleEscrowLock", state.toggleEscrowLockProposalSettings],
217
+ ["updateFields", state.updateFieldsProposalSettings],
218
+ ];
219
+ const proposalSettings: Record<string, { fee: bigint; power: bigint; duration: bigint; participation: bigint; approval: bigint }> = {};
220
+ for (const [key, ps] of psEntries) {
221
+ if (ps) proposalSettings[key] = ps;
222
+ }
223
+
224
+ // Revenue splits
225
+ const revenueSplits = state.revenueSplits?.map(([[wallet, escrow], type, value]) => ({
226
+ wallet: wallet.toString(),
227
+ escrow: escrow || undefined,
228
+ type: type === 20 ? "percentage" : type === 30 ? "remainder" : `unknown(${type})`,
229
+ value,
230
+ percentage: type === 20
231
+ ? Number(value) / 1000
232
+ : type === 30
233
+ ? Number(100_000n - (state.revenueSplits ?? []).reduce((sum, [, t, v]) => t === 20 ? sum + v : sum, 0n)) / 1000
234
+ : undefined,
235
+ })) ?? [];
236
+
237
+ // Token supply
238
+ const tokenSupply: Record<string, { total: bigint; circulating: bigint; decimals: number }> = {};
239
+ if (aktaSupply) tokenSupply.akta = aktaSupply;
240
+ if (bonesSupply) tokenSupply.bones = bonesSupply;
241
+
242
+ return {
243
+ network,
244
+ appId,
245
+ version: state.version ?? null,
246
+ state: state.state !== undefined ? daoStateLabel(state.state) : null,
247
+ wallet: state.wallet ?? null,
248
+ assets: {
249
+ akta: state.akitaAssets?.akta ?? ids.akta,
250
+ bones: state.akitaAssets?.bones ?? ids.bones,
251
+ nextProposal: state.proposalId ?? null,
252
+ actionLimit: state.proposalActionLimit ?? null,
253
+ minRewardsImpact: state.minRewardsImpact ?? null,
254
+ },
255
+ tokenSupply,
256
+ apps,
257
+ proposalSettings,
258
+ revenueSplits,
259
+ };
260
+ }
261
+
179
262
  // ── Helpers ────────────────────────────────────────────────────
180
263
 
181
264
  function renderAppTable(state: Partial<AkitaDaoGlobalState>, network: AkitaNetwork): string[] {
@@ -8,7 +8,7 @@ import {
8
8
  } from "../../formatting";
9
9
  import { renderPanel, splitWidth } from "../panels";
10
10
  import { padEndVisible, visibleLength } from "../../output";
11
- import type { View, ViewContext } from "../types";
11
+ import type { LoadResult, View, ViewContext } from "../types";
12
12
 
13
13
  const PERCENTAGE_RE = /Percentage|Tax/i;
14
14
  const FEE_RE = /Fee$/i;
@@ -24,60 +24,65 @@ function feeLabel(key: string): string {
24
24
  }
25
25
 
26
26
  export const feesView: View = {
27
- async load(ctx: ViewContext): Promise<string[]> {
27
+ async load(ctx: ViewContext): Promise<LoadResult> {
28
28
  const { dao, width } = ctx;
29
29
  const state = await dao.getGlobalState();
30
30
 
31
31
  const groups = collectFeeGroups(state);
32
+ const rawGroups = collectRawFeeGroups(state);
32
33
 
33
34
  if (groups.length === 0) {
34
- return [
35
- "",
36
- ...renderPanel([" No fee data available."], { title: "Fees", width }),
37
- ];
35
+ return {
36
+ lines: [
37
+ "",
38
+ ...renderPanel([" No fee data available."], { title: "Fees", width }),
39
+ ],
40
+ data: {},
41
+ };
38
42
  }
39
43
 
44
+ let lines: string[];
40
45
  if (width < 80) {
41
- return renderSingleColumn(groups, width);
42
- }
43
-
44
- const cols = 2;
45
- const colGap = 2;
46
- const panelGap = 1;
47
- const [leftW, rightW] = splitWidth(width, cols, colGap);
48
-
49
- // Compute max key width per grid column
50
- const colKeyWidths: number[] = Array(cols).fill(0);
51
- for (let i = 0; i < groups.length; i++) {
52
- const col = i % cols;
53
- for (const [key] of groups[i].pairs) {
54
- colKeyWidths[col] = Math.max(colKeyWidths[col], key.length);
46
+ lines = renderSingleColumn(groups, width);
47
+ } else {
48
+ const cols = 2;
49
+ const colGap = 2;
50
+ const panelGap = 1;
51
+ const [leftW, rightW] = splitWidth(width, cols, colGap);
52
+
53
+ // Compute max key width per grid column
54
+ const colKeyWidths: number[] = Array(cols).fill(0);
55
+ for (let i = 0; i < groups.length; i++) {
56
+ const col = i % cols;
57
+ for (const [key] of groups[i].pairs) {
58
+ colKeyWidths[col] = Math.max(colKeyWidths[col], key.length);
59
+ }
55
60
  }
56
- }
57
61
 
58
- // Split groups into independent columns
59
- const colWidths = [leftW, rightW];
60
- const columns: string[][] = [[], []];
61
- for (let i = 0; i < groups.length; i++) {
62
- const col = i % cols;
63
- if (columns[col].length > 0) {
64
- for (let g = 0; g < panelGap; g++) columns[col].push("");
62
+ // Split groups into independent columns
63
+ const colWidths = [leftW, rightW];
64
+ const columns: string[][] = [[], []];
65
+ for (let i = 0; i < groups.length; i++) {
66
+ const col = i % cols;
67
+ if (columns[col].length > 0) {
68
+ for (let g = 0; g < panelGap; g++) columns[col].push("");
69
+ }
70
+ const content = renderFeeKV(groups[i].pairs, colKeyWidths[col]);
71
+ columns[col].push(...renderPanel(content, { title: groups[i].title, width: colWidths[col] }));
65
72
  }
66
- const content = renderFeeKV(groups[i].pairs, colKeyWidths[col]);
67
- columns[col].push(...renderPanel(content, { title: groups[i].title, width: colWidths[col] }));
68
- }
69
73
 
70
- // Merge columns side-by-side
71
- const maxHeight = Math.max(columns[0].length, columns[1].length);
72
- const gapStr = " ".repeat(colGap);
73
- const lines: string[] = [""];
74
- for (let i = 0; i < maxHeight; i++) {
75
- const left = i < columns[0].length ? padEndVisible(columns[0][i], leftW) : " ".repeat(leftW);
76
- const right = i < columns[1].length ? columns[1][i] : "";
77
- lines.push(left + gapStr + right);
74
+ // Merge columns side-by-side
75
+ const maxHeight = Math.max(columns[0].length, columns[1].length);
76
+ const gapStr = " ".repeat(colGap);
77
+ lines = [""];
78
+ for (let i = 0; i < maxHeight; i++) {
79
+ const left = i < columns[0].length ? padEndVisible(columns[0][i], leftW) : " ".repeat(leftW);
80
+ const right = i < columns[1].length ? columns[1][i] : "";
81
+ lines.push(left + gapStr + right);
82
+ }
78
83
  }
79
84
 
80
- return lines;
85
+ return { lines, data: rawGroups };
81
86
  },
82
87
  };
83
88
 
@@ -97,6 +102,23 @@ function renderFeeKV(pairs: [string, string][], keyWidth?: number): string[] {
97
102
  return pairs.map(([key, value]) => ` ${theme.label(key.padEnd(w))} ${value}`);
98
103
  }
99
104
 
105
+ function collectRawFeeGroups(state: Partial<AkitaDaoGlobalState>): Record<string, Record<string, bigint>> {
106
+ const feeGroups: [string, Record<string, bigint> | undefined][] = [
107
+ ["walletFees", state.walletFees as Record<string, bigint> | undefined],
108
+ ["socialFees", state.socialFees as Record<string, bigint> | undefined],
109
+ ["stakingFees", state.stakingFees as Record<string, bigint> | undefined],
110
+ ["subscriptionFees", state.subscriptionFees as Record<string, bigint> | undefined],
111
+ ["swapFees", state.swapFees as Record<string, bigint> | undefined],
112
+ ["nftFees", state.nftFees as Record<string, bigint> | undefined],
113
+ ];
114
+
115
+ const result: Record<string, Record<string, bigint>> = {};
116
+ for (const [name, fees] of feeGroups) {
117
+ if (fees) result[name] = fees;
118
+ }
119
+ return result;
120
+ }
121
+
100
122
  function collectFeeGroups(state: Partial<AkitaDaoGlobalState>): KVGroup[] {
101
123
  const feeGroups: [string, Record<string, bigint> | undefined][] = [
102
124
  ["Wallet Fees", state.walletFees as Record<string, bigint> | undefined],
@@ -3,7 +3,9 @@ import theme from "../../theme";
3
3
  import {
4
4
  truncateAddress,
5
5
  formatTimestamp,
6
+ formatCID,
6
7
  proposalStatusLabel,
8
+ proposalActionLabel,
7
9
  colorStatus,
8
10
  } from "../../formatting";
9
11
  import { renderPanel } from "../panels";
@@ -112,10 +114,13 @@ export const proposalsListView: View = {
112
114
  if (_cursor >= _proposalIds.length) _cursor = 0;
113
115
 
114
116
  if (_cachedEntries.length === 0) {
115
- return [
116
- "",
117
- ...renderPanel([" No proposals found."], { title: "Proposals", width }),
118
- ];
117
+ return {
118
+ lines: [
119
+ "",
120
+ ...renderPanel([" No proposals found."], { title: "Proposals", width }),
121
+ ],
122
+ data: { proposals: [], selected: null },
123
+ };
119
124
  }
120
125
 
121
126
  // Fetch selected proposal detail (cached)
@@ -138,9 +143,15 @@ export const proposalsListView: View = {
138
143
  }
139
144
  }
140
145
 
146
+ // Build structured data for JSON mode
147
+ const data = buildProposalsData(_cachedEntries, detailData, selectedId);
148
+
141
149
  // Narrow: single column (list, then detail below)
142
150
  if (width < 80) {
143
- return renderSingleColumn(_cachedEntries, detailData, selectedId, network, width);
151
+ return {
152
+ lines: renderSingleColumn(_cachedEntries, detailData, selectedId, network, width),
153
+ data,
154
+ };
144
155
  }
145
156
 
146
157
  // Wide: two-panel layout (left scrolls, right fixed)
@@ -159,10 +170,48 @@ export const proposalsListView: View = {
159
170
  return {
160
171
  lines: ["", ...listPanel],
161
172
  fixedRight: ["", ...detailLines],
173
+ data,
162
174
  };
163
175
  },
164
176
  };
165
177
 
178
+ // ── Structured data for JSON mode ──────────────────────────────
179
+
180
+ function buildProposalsData(
181
+ entries: ListEntry[],
182
+ detailData: ProposalData | null,
183
+ selectedId: bigint | undefined,
184
+ ) {
185
+ const proposals = entries.map((e) => ({
186
+ id: e.id,
187
+ status: proposalStatusLabel(e.status),
188
+ creator: e.creator,
189
+ votes: e.votes,
190
+ actionCount: e.actionCount,
191
+ created: e.created,
192
+ }));
193
+
194
+ let selected = null;
195
+ if (detailData && selectedId !== undefined) {
196
+ selected = {
197
+ id: selectedId,
198
+ status: proposalStatusLabel(detailData.status),
199
+ creator: detailData.creator,
200
+ cid: formatCID(detailData.cid),
201
+ created: detailData.created,
202
+ votingTs: detailData.votingTs,
203
+ votes: detailData.votes,
204
+ feesPaid: detailData.feesPaid,
205
+ actions: detailData.actions.map((a) => ({
206
+ ...a,
207
+ type: proposalActionLabel(a.type),
208
+ })),
209
+ };
210
+ }
211
+
212
+ return { proposals, selected };
213
+ }
214
+
166
215
  // ── Left panel: compact proposals list ─────────────────────────
167
216
 
168
217
  function renderListPanel(entries: ListEntry[], width: number): string[] {
@@ -171,11 +220,12 @@ function renderListPanel(entries: ListEntry[], width: number): string[] {
171
220
  return [
172
221
  marker + e.id.toString(),
173
222
  colorStatus(proposalStatusLabel(e.status)),
223
+ truncateAddress(e.creator),
174
224
  e.actionCount.toString(),
175
225
  ];
176
226
  });
177
227
 
178
- const content = renderColumns([" ID", "Status", "Act"], rows);
228
+ const content = renderColumns([" ID", "Status", "Proposer", "Actions"], rows);
179
229
  return renderPanel(content, { title: `Proposals (${entries.length})`, width });
180
230
  }
181
231
 
@@ -192,27 +192,152 @@ export const walletView: View = {
192
192
 
193
193
  const selectedAccount = accounts[_accountIdx];
194
194
 
195
+ const selectedAppId = selectedAccount.escrowName === ""
196
+ ? wallet.appId
197
+ : cache.escrows.find(([n]) => n === selectedAccount.escrowName)?.[1].id ?? 0n;
198
+
199
+ // Build structured data for JSON mode
200
+ const data = buildWalletData(cache, network, accounts, selectedAccount, selectedAppId, wallet.appId);
201
+
195
202
  if (width < 80) {
196
- return renderSingleColumn(cache, network, accounts, selectedAccount, wallet.appId);
203
+ return {
204
+ lines: renderSingleColumn(cache, network, accounts, selectedAccount, wallet.appId),
205
+ data,
206
+ };
197
207
  }
198
208
 
199
209
  // Two-panel layout
200
210
  const [leftW, rightW] = splitWidth(width, 2);
201
211
 
202
- const selectedAppId = selectedAccount.escrowName === ""
203
- ? wallet.appId
204
- : cache.escrows.find(([n]) => n === selectedAccount.escrowName)?.[1].id ?? 0n;
205
-
206
212
  const leftLines = renderLeftPanel(cache, network, accounts, leftW, wallet.appId);
207
213
  const rightLines = renderRightPanel(cache, network, selectedAccount, selectedAppId, rightW);
208
214
 
209
215
  return {
210
216
  lines: ["", ...leftLines],
211
217
  fixedRight: ["", ...rightLines],
218
+ data,
212
219
  };
213
220
  },
214
221
  };
215
222
 
223
+ // ── Structured data for JSON mode ────────────────────────────────
224
+
225
+ function buildWalletData(
226
+ cache: WalletCache,
227
+ network: AkitaNetwork,
228
+ accounts: { name: string; address: string; escrowName: string }[],
229
+ selectedAccount: { name: string; address: string; escrowName: string },
230
+ selectedAppId: bigint,
231
+ walletAppId: bigint,
232
+ ) {
233
+ const gs = cache.globalState;
234
+
235
+ const info = {
236
+ version: gs.version ?? null,
237
+ admin: gs.admin ?? null,
238
+ domain: typeof gs.domain === "string" && gs.domain ? gs.domain : null,
239
+ nickname: typeof gs.nickname === "string" && gs.nickname ? gs.nickname : null,
240
+ dao: gs.akitaDao ?? null,
241
+ factory: typeof gs.factoryApp === "bigint" && (gs.factoryApp as bigint) > 0n ? gs.factoryApp : null,
242
+ referrer: typeof gs.referrer === "string" && !isZeroAddress(gs.referrer) ? gs.referrer : null,
243
+ };
244
+
245
+ const mainAddr = accounts[0].address;
246
+ const accountsData = accounts.map((acct) => {
247
+ const bal = cache.balances.get(acct.address);
248
+ return {
249
+ name: acct.name,
250
+ appId: acct.escrowName === ""
251
+ ? walletAppId
252
+ : cache.escrows.find(([n]) => n === acct.escrowName)?.[1].id ?? null,
253
+ address: acct.address,
254
+ balances: bal ?? null,
255
+ };
256
+ });
257
+
258
+ const escrowsData = cache.escrows.map(([name, esc]) => ({
259
+ name,
260
+ appId: esc.id,
261
+ locked: esc.locked,
262
+ }));
263
+
264
+ // Selected account details
265
+ const escrowFilter = selectedAccount.escrowName;
266
+
267
+ const filteredPlugins = cache.plugins.filter(([key]) => {
268
+ const parsed = parsePluginKey(key);
269
+ return escrowFilter === "" ? parsed.escrow === "" : parsed.escrow === escrowFilter;
270
+ });
271
+
272
+ const plugins = filteredPlugins.map(([key, p]) => {
273
+ const { pluginId, caller } = parsePluginKey(key);
274
+ return {
275
+ key,
276
+ pluginId: pluginId ?? null,
277
+ pluginName: pluginId ? getAppName(pluginId, network) ?? null : null,
278
+ caller: caller || null,
279
+ admin: p.admin,
280
+ delegationType: delegationTypeLabel(p.delegationType),
281
+ coverFees: p.coverFees,
282
+ canReclaim: p.canReclaim,
283
+ useExecutionKey: p.useExecutionKey,
284
+ useRounds: p.useRounds,
285
+ cooldown: p.cooldown,
286
+ lastCalled: p.lastCalled,
287
+ start: p.start,
288
+ lastValid: p.lastValid,
289
+ methods: p.methods.map((m) => ({
290
+ name: resolveMethodSelector(m.name),
291
+ cooldown: m.cooldown,
292
+ lastCalled: m.lastCalled,
293
+ })),
294
+ };
295
+ });
296
+
297
+ const namedPlugins = cache.namedPlugins.map(([name, np]) => ({
298
+ name,
299
+ plugin: np.plugin,
300
+ caller: np.caller,
301
+ escrow: np.escrow || null,
302
+ }));
303
+
304
+ const filteredAllowances = cache.allowances.filter(([key]) => {
305
+ if (escrowFilter === "") return true;
306
+ return key.includes(escrowFilter);
307
+ });
308
+
309
+ const allowances = filteredAllowances.map(([key, a]) => ({
310
+ key,
311
+ type: a.type,
312
+ amount: a.amount ?? null,
313
+ spent: a.spent ?? null,
314
+ rate: a.rate ?? null,
315
+ max: a.max ?? null,
316
+ interval: a.interval ?? null,
317
+ }));
318
+
319
+ const executions = cache.executions.map(([key, e]) => ({
320
+ lease: key,
321
+ firstValid: e.firstValid,
322
+ lastValid: e.lastValid,
323
+ }));
324
+
325
+ return {
326
+ info,
327
+ accounts: accountsData,
328
+ escrows: escrowsData,
329
+ selectedAccount: {
330
+ name: selectedAccount.name,
331
+ appId: selectedAppId,
332
+ address: selectedAccount.address,
333
+ plugins,
334
+ namedPlugins,
335
+ allowances,
336
+ executions,
337
+ },
338
+ };
339
+ }
340
+
216
341
  // ── Left panel: Wallet Info + Account List ─────────────────────
217
342
 
218
343
  function renderLeftPanel(