@akta/dao-cli 0.1.0 → 0.1.2

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
 
Binary file
Binary file
Binary file
Binary file
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.2",
4
4
  "description": "Read-only CLI and TUI for querying Akita DAO state on Algorand",
5
5
  "type": "module",
6
6
  "bin": {
@@ -8,7 +8,8 @@
8
8
  },
9
9
  "scripts": {
10
10
  "start": "bun run src/index.ts",
11
- "typecheck": "tsc --noEmit"
11
+ "typecheck": "tsc --noEmit",
12
+ "style-screenshots": "bun run scripts/style-screenshots.ts"
12
13
  },
13
14
  "dependencies": {
14
15
  "@akta/sdk": "0.0.1",
@@ -9,13 +9,32 @@ import {
9
9
  formatBigInt,
10
10
  daoStateLabel,
11
11
  resolveAppName,
12
+ getAppName,
12
13
  colorState,
13
14
  } from "../formatting";
14
15
 
15
16
  export async function stateCommand(dao: AkitaDaoSDK, network: AkitaNetwork, json: boolean): Promise<void> {
16
17
  const state = await dao.getGlobalState();
17
18
 
18
- if (json) return printJson(state);
19
+ if (json) {
20
+ let pluginProposalSettings: { plugin: bigint; pluginName: string; account: string; fee: bigint; power: bigint; duration: bigint; participation: bigint; approval: bigint }[] = [];
21
+ try {
22
+ const pluginsMap = await dao.client.state.box.plugins.getMap();
23
+ pluginProposalSettings = [...pluginsMap.entries()].map(([key, ps]) => ({
24
+ plugin: key.plugin,
25
+ pluginName: (getAppName(key.plugin, network) ?? key.plugin.toString()).replace(/ Plugin$/, ""),
26
+ account: key.escrow || "Main",
27
+ fee: ps.fee,
28
+ power: ps.power,
29
+ duration: ps.duration,
30
+ participation: ps.participation,
31
+ approval: ps.approval,
32
+ }));
33
+ } catch {
34
+ // Box may be empty or inaccessible
35
+ }
36
+ return printJson({ ...state, pluginProposalSettings });
37
+ }
19
38
 
20
39
  header("Core Settings");
21
40
  printKV([
@@ -38,6 +57,7 @@ export async function stateCommand(dao: AkitaDaoSDK, network: AkitaNetwork, json
38
57
  printAppLists(state, network);
39
58
  printFees(state);
40
59
  printProposalSettings(state);
60
+ await printPluginProposalSettings(dao, network);
41
61
  printRevenueSplits(state, network);
42
62
  }
43
63
 
@@ -152,6 +172,31 @@ function printProposalSettings(state: Partial<AkitaDaoGlobalState>): void {
152
172
  printColumns(["Category", "Fee", "Power", "Duration", "Participation", "Approval"], rows);
153
173
  }
154
174
 
175
+ async function printPluginProposalSettings(dao: AkitaDaoSDK, network: AkitaNetwork): Promise<void> {
176
+ let pluginsMap: Map<{ plugin: bigint; escrow: string }, { fee: bigint; power: bigint; duration: bigint; participation: bigint; approval: bigint }>;
177
+ try {
178
+ pluginsMap = await dao.client.state.box.plugins.getMap();
179
+ } catch {
180
+ return;
181
+ }
182
+ if (pluginsMap.size === 0) return;
183
+
184
+ header("Plugin Proposal Settings");
185
+ const rows = [...pluginsMap.entries()].map(([key, ps]) => {
186
+ const name = (getAppName(key.plugin, network) ?? key.plugin.toString()).replace(/ Plugin$/, "");
187
+ return [
188
+ name,
189
+ key.escrow || "Main",
190
+ formatMicroAlgo(ps.fee),
191
+ formatBigInt(ps.power),
192
+ formatDuration(ps.duration),
193
+ formatBasisPoints(ps.participation),
194
+ formatBasisPoints(ps.approval),
195
+ ];
196
+ });
197
+ printColumns(["Plugin", "Account", "Fee", "Power", "Duration", "Participation", "Approval"], rows);
198
+ }
199
+
155
200
  function printRevenueSplits(state: Partial<AkitaDaoGlobalState>, network: AkitaNetwork): void {
156
201
  if (!state.revenueSplits || state.revenueSplits.length === 0) return;
157
202
 
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,14 +15,26 @@ 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
+ import type { AkitaDaoSDK } from "@akta/sdk/dao";
20
+
21
+ type PluginsMap = Awaited<ReturnType<AkitaDaoSDK["client"]["state"]["box"]["plugins"]["getMap"]>>;
19
22
 
20
23
  export const daoView: View = {
21
- async load(ctx: ViewContext): Promise<string[]> {
24
+ async load(ctx: ViewContext): Promise<LoadResult> {
22
25
  const { dao, network, width } = ctx;
23
26
  const state = await dao.getGlobalState();
24
27
  const ids = getNetworkAppIds(network);
25
28
 
29
+ // Fetch per-plugin proposal settings from box storage
30
+ let pluginsMap: PluginsMap | null = null;
31
+ try {
32
+ pluginsMap = await dao.client.state.box.plugins.getMap();
33
+ if (pluginsMap.size === 0) pluginsMap = null;
34
+ } catch {
35
+ // Box may be empty or inaccessible
36
+ }
37
+
26
38
  // Fetch supply data for AKTA & BONES
27
39
  let aktaSupply: SupplyInfo | null = null;
28
40
  let bonesSupply: SupplyInfo | null = null;
@@ -53,75 +65,86 @@ export const daoView: View = {
53
65
  // Asset info fetch failed — skip supply charts
54
66
  }
55
67
 
56
- if (width < 80) {
57
- return renderSingleColumn(state, network, ids, width, aktaSupply, bonesSupply);
58
- }
68
+ // Build structured data for JSON mode
69
+ const data = buildDaoData(state, network, ids, dao.appId, aktaSupply, bonesSupply, pluginsMap);
59
70
 
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]);
71
+ let lines: string[];
72
+ if (width < 80) {
73
+ lines = renderSingleColumn(state, network, ids, width, aktaSupply, bonesSupply, pluginsMap);
90
74
  } 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 }));
75
+ const gridRows: string[][][] = [];
76
+
77
+ // Row 1: DAO info + Assets + Token Supply
78
+ const hasSupply = aktaSupply || bonesSupply;
79
+ const colCount = hasSupply ? 3 : 2;
80
+ const colWidths = splitWidth(width, colCount);
81
+
82
+ const infoContent = renderKV([
83
+ ["Network", network],
84
+ ["App ID", dao.appId.toString()],
85
+ ["Version", state.version ?? "-"],
86
+ ["State", state.state !== undefined ? colorState(daoStateLabel(state.state)) : "-"],
87
+ ["Wallet", state.wallet ? resolveAppName(state.wallet, network) : "-"],
88
+ ]);
89
+ const infoPanel = renderPanel(infoContent, { title: "Akita DAO", width: colWidths[0] });
90
+
91
+ const assetsContent = renderKV([
92
+ ["AKTA", state.akitaAssets?.akta?.toString() ?? ids.akta.toString()],
93
+ ["BONES", state.akitaAssets?.bones?.toString() ?? ids.bones.toString()],
94
+ ["Next Proposal", state.proposalId?.toString() ?? "-"],
95
+ ["Action Limit", state.proposalActionLimit?.toString() ?? "-"],
96
+ ["Min Rewards", state.minRewardsImpact?.toString() ?? "-"],
97
+ ]);
98
+ const assetsPanel = renderPanel(assetsContent, { title: "Assets", width: colWidths[1] });
99
+
100
+ if (hasSupply) {
101
+ const supplyW = colWidths[2];
102
+ const supplyContent = renderSupplyCharts(aktaSupply, bonesSupply, supplyW - 4);
103
+ const supplyPanel = renderPanel(supplyContent, { title: "Token Supply", width: supplyW });
104
+ gridRows.push([infoPanel, assetsPanel, supplyPanel]);
105
+ } else {
106
+ gridRows.push([infoPanel, assetsPanel]);
116
107
  }
117
- if (rightPanels.length === 0) {
118
- rightPanels.push(...renderPanel([" No proposal settings"], { title: "Proposal Settings", width: rightPanelW }));
108
+
109
+ // Row 3: App IDs (40%) + Proposal Settings / Revenue Splits stacked (60%)
110
+ const appLines = renderAppTable(state, network);
111
+ const proposalLines = renderProposalSettings(state);
112
+
113
+ if (appLines.length > 0 || proposalLines.length > 0) {
114
+ // ~40/60 split
115
+ const appW = Math.floor((width - 2) * 0.4);
116
+ const rightPanelW = width - appW - 2;
117
+ const revLines = renderRevenueSplits(state, network, rightPanelW - 4);
118
+
119
+ const leftPanel = appLines.length > 0
120
+ ? renderPanel(appLines, { title: "App IDs", width: appW })
121
+ : renderPanel([" No app ID data"], { title: "App IDs", width: appW });
122
+
123
+ // Stack Proposal Settings + Plugin Proposal Settings + Revenue Splits into one right column
124
+ const rightPanels: string[] = [];
125
+ if (proposalLines.length > 0) {
126
+ rightPanels.push(...renderPanel(proposalLines, { title: "Proposal Settings", width: rightPanelW }));
127
+ }
128
+ const pluginPsLines = renderPluginProposalSettings(pluginsMap, network);
129
+ if (pluginPsLines.length > 0) {
130
+ if (rightPanels.length > 0) rightPanels.push("");
131
+ rightPanels.push(...renderPanel(pluginPsLines, { title: "Plugin Proposal Settings", width: rightPanelW }));
132
+ }
133
+ if (revLines.length > 0) {
134
+ if (rightPanels.length > 0) rightPanels.push("");
135
+ rightPanels.push(...renderPanel(revLines, { title: "Revenue Splits", width: rightPanelW }));
136
+ }
137
+ if (rightPanels.length === 0) {
138
+ rightPanels.push(...renderPanel([" No proposal settings"], { title: "Proposal Settings", width: rightPanelW }));
139
+ }
140
+
141
+ gridRows.push([leftPanel, rightPanels]);
119
142
  }
120
143
 
121
- gridRows.push([leftPanel, rightPanels]);
144
+ lines = ["", ...renderPanelGrid(gridRows, { rowGap: 1 })];
122
145
  }
123
146
 
124
- return ["", ...renderPanelGrid(gridRows, { rowGap: 1 })];
147
+ return { lines, data };
125
148
  },
126
149
  };
127
150
 
@@ -134,6 +157,7 @@ function renderSingleColumn(
134
157
  width: number,
135
158
  aktaSupply: SupplyInfo | null,
136
159
  bonesSupply: SupplyInfo | null,
160
+ pluginsMap: PluginsMap | null,
137
161
  ): string[] {
138
162
  const lines: string[] = [""];
139
163
 
@@ -167,6 +191,12 @@ function renderSingleColumn(
167
191
  lines.push(...renderPanel(proposalLines, { title: "Proposal Settings", width }));
168
192
  }
169
193
 
194
+ const pluginPsLines = renderPluginProposalSettings(pluginsMap, network);
195
+ if (pluginPsLines.length > 0) {
196
+ lines.push("");
197
+ lines.push(...renderPanel(pluginPsLines, { title: "Plugin Proposal Settings", width }));
198
+ }
199
+
170
200
  const revLines = renderRevenueSplits(state, network, width - 4);
171
201
  if (revLines.length > 0) {
172
202
  lines.push("");
@@ -176,6 +206,94 @@ function renderSingleColumn(
176
206
  return lines;
177
207
  }
178
208
 
209
+ // ── Structured data for JSON mode ───────────────────────────────
210
+
211
+ function buildDaoData(
212
+ state: Partial<AkitaDaoGlobalState>,
213
+ network: AkitaNetwork,
214
+ ids: { akta: bigint; bones: bigint },
215
+ appId: bigint,
216
+ aktaSupply: SupplyInfo | null,
217
+ bonesSupply: SupplyInfo | null,
218
+ pluginsMap: PluginsMap | null,
219
+ ) {
220
+ // App ID lists
221
+ const appSections: [string, Record<string, bigint> | undefined][] = [
222
+ ["core", state.akitaAppList as Record<string, bigint> | undefined],
223
+ ["social", state.akitaSocialAppList as Record<string, bigint> | undefined],
224
+ ["plugins", state.pluginAppList as Record<string, bigint> | undefined],
225
+ ["other", state.otherAppList as Record<string, bigint> | undefined],
226
+ ];
227
+ const apps: Record<string, Record<string, bigint>> = {};
228
+ for (const [category, list] of appSections) {
229
+ if (list) apps[category] = list;
230
+ }
231
+
232
+ // Proposal settings
233
+ const psEntries: [string, { fee: bigint; power: bigint; duration: bigint; participation: bigint; approval: bigint } | undefined][] = [
234
+ ["upgradeApp", state.upgradeAppProposalSettings],
235
+ ["addPlugin", state.addPluginProposalSettings],
236
+ ["removePlugin", state.removePluginProposalSettings],
237
+ ["removeExecutePlugin", state.removeExecutePluginProposalSettings],
238
+ ["addAllowances", state.addAllowancesProposalSettings],
239
+ ["removeAllowances", state.removeAllowancesProposalSettings],
240
+ ["newEscrow", state.newEscrowProposalSettings],
241
+ ["toggleEscrowLock", state.toggleEscrowLockProposalSettings],
242
+ ["updateFields", state.updateFieldsProposalSettings],
243
+ ];
244
+ const proposalSettings: Record<string, { fee: bigint; power: bigint; duration: bigint; participation: bigint; approval: bigint }> = {};
245
+ for (const [key, ps] of psEntries) {
246
+ if (ps) proposalSettings[key] = ps;
247
+ }
248
+
249
+ // Revenue splits
250
+ const revenueSplits = state.revenueSplits?.map(([[wallet, escrow], type, value]) => ({
251
+ wallet: wallet.toString(),
252
+ escrow: escrow || undefined,
253
+ type: type === 20 ? "percentage" : type === 30 ? "remainder" : `unknown(${type})`,
254
+ value,
255
+ percentage: type === 20
256
+ ? Number(value) / 1000
257
+ : type === 30
258
+ ? Number(100_000n - (state.revenueSplits ?? []).reduce((sum, [, t, v]) => t === 20 ? sum + v : sum, 0n)) / 1000
259
+ : undefined,
260
+ })) ?? [];
261
+
262
+ // Token supply
263
+ const tokenSupply: Record<string, { total: bigint; circulating: bigint; decimals: number }> = {};
264
+ if (aktaSupply) tokenSupply.akta = aktaSupply;
265
+ if (bonesSupply) tokenSupply.bones = bonesSupply;
266
+
267
+ return {
268
+ network,
269
+ appId,
270
+ version: state.version ?? null,
271
+ state: state.state !== undefined ? daoStateLabel(state.state) : null,
272
+ wallet: state.wallet ?? null,
273
+ assets: {
274
+ akta: state.akitaAssets?.akta ?? ids.akta,
275
+ bones: state.akitaAssets?.bones ?? ids.bones,
276
+ nextProposal: state.proposalId ?? null,
277
+ actionLimit: state.proposalActionLimit ?? null,
278
+ minRewardsImpact: state.minRewardsImpact ?? null,
279
+ },
280
+ tokenSupply,
281
+ apps,
282
+ proposalSettings,
283
+ pluginProposalSettings: pluginsMap ? [...pluginsMap.entries()].map(([key, ps]) => ({
284
+ plugin: key.plugin,
285
+ pluginName: (getAppName(key.plugin, network) ?? key.plugin.toString()).replace(/ Plugin$/, ""),
286
+ account: key.escrow || "Main",
287
+ fee: ps.fee,
288
+ power: ps.power,
289
+ duration: ps.duration,
290
+ participation: ps.participation,
291
+ approval: ps.approval,
292
+ })) : [],
293
+ revenueSplits,
294
+ };
295
+ }
296
+
179
297
  // ── Helpers ────────────────────────────────────────────────────
180
298
 
181
299
  function renderAppTable(state: Partial<AkitaDaoGlobalState>, network: AkitaNetwork): string[] {
@@ -229,6 +347,25 @@ function renderProposalSettings(state: Partial<AkitaDaoGlobalState>): string[] {
229
347
  return renderColumns(["Cat", "Fee", "Pwr", "Dur", "Part", "Appr"], rows);
230
348
  }
231
349
 
350
+ function renderPluginProposalSettings(pluginsMap: PluginsMap | null, network: AkitaNetwork): string[] {
351
+ if (!pluginsMap || pluginsMap.size === 0) return [];
352
+
353
+ const rows = [...pluginsMap.entries()].map(([key, ps]) => {
354
+ const name = (getAppName(key.plugin, network) ?? key.plugin.toString()).replace(/ Plugin$/, "");
355
+ return [
356
+ name,
357
+ key.escrow || "Main",
358
+ formatMicroAlgo(ps.fee),
359
+ formatBigInt(ps.power),
360
+ formatDuration(ps.duration),
361
+ inlineBar(ps.participation),
362
+ inlineBar(ps.approval),
363
+ ];
364
+ });
365
+
366
+ return renderColumns(["Plugin", "Account", "Fee", "Pwr", "Dur", "Part", "Appr"], rows);
367
+ }
368
+
232
369
  const SPLIT_COLORS = theme.splitColors;
233
370
 
234
371
  function renderRevenueSplits(state: Partial<AkitaDaoGlobalState>, network: AkitaNetwork, barWidth: number): string[] {
@@ -293,7 +430,7 @@ function renderRevenueSplits(state: Partial<AkitaDaoGlobalState>, network: Akita
293
430
 
294
431
  // ── Inline bar for basis-point percentages ─────────────────────
295
432
 
296
- const INLINE_BAR_WIDTH = 8;
433
+ const INLINE_BAR_WIDTH = 10;
297
434
 
298
435
  function inlineBar(bp: bigint): string {
299
436
  const pct = Number(bp) / 1000;
@@ -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(