@thecorporation/agent 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1510 @@
1
+ /**
2
+ * TheCorporation — Pi Extension
3
+ *
4
+ * Registers 30 corporate operations tools, slash commands, and write-tool
5
+ * confirmation gating for the pi coding agent.
6
+ */
7
+
8
+ import { Type, type Static } from "@sinclair/typebox";
9
+ import { readFileSync, existsSync, writeFileSync, mkdirSync } from "fs";
10
+ import { join } from "path";
11
+ import { homedir } from "os";
12
+ import { Text } from "@mariozechner/pi-tui";
13
+ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
14
+
15
+ // ── Config & API Client ─────────────────────────────────────────
16
+
17
+ interface CorpConfig {
18
+ api_url: string;
19
+ api_key: string;
20
+ workspace_id: string;
21
+ active_entity_id?: string;
22
+ user?: { name?: string; email?: string };
23
+ }
24
+
25
+ function loadConfig(): CorpConfig {
26
+ const configPath = join(process.env.CORP_CONFIG_DIR || join(homedir(), ".corp"), "config.json");
27
+ const defaults: CorpConfig = {
28
+ api_url: process.env.CORP_API_URL || "https://api.thecorporation.ai",
29
+ api_key: "",
30
+ workspace_id: "",
31
+ };
32
+ if (!existsSync(configPath)) return defaults;
33
+ try {
34
+ const saved = JSON.parse(readFileSync(configPath, "utf-8"));
35
+ return { ...defaults, ...saved };
36
+ } catch {
37
+ return defaults;
38
+ }
39
+ }
40
+
41
+ async function apiCall(method: string, path: string, body?: unknown): Promise<any> {
42
+ const cfg = loadConfig();
43
+ const url = cfg.api_url.replace(/\/+$/, "") + path;
44
+ const headers: Record<string, string> = {
45
+ Authorization: `Bearer ${cfg.api_key}`,
46
+ "Content-Type": "application/json",
47
+ Accept: "application/json",
48
+ };
49
+ const opts: RequestInit = { method, headers, signal: AbortSignal.timeout(30_000) };
50
+ if (body !== undefined) opts.body = JSON.stringify(body);
51
+ const resp = await fetch(url, opts);
52
+ if (!resp.ok) {
53
+ const text = await resp.text().catch(() => "");
54
+ throw new Error(`API ${resp.status}: ${text || resp.statusText}`);
55
+ }
56
+ return resp.json();
57
+ }
58
+
59
+ function wsId(): string {
60
+ return loadConfig().workspace_id;
61
+ }
62
+
63
+ // ── Helpers ──────────────────────────────────────────────────────
64
+
65
+ function centsToUsd(cents: number): string {
66
+ return "$" + (cents / 100).toLocaleString("en-US", { minimumFractionDigits: 0, maximumFractionDigits: 0 });
67
+ }
68
+
69
+ /** Tools that are auto-approved (no confirmation needed). */
70
+ const READ_ONLY_TOOLS = new Set([
71
+ "get_workspace_status",
72
+ "list_entities",
73
+ "get_cap_table",
74
+ "list_documents",
75
+ "list_safe_notes",
76
+ "list_agents",
77
+ "get_checklist",
78
+ "get_document_link",
79
+ "get_signing_link",
80
+ "list_obligations",
81
+ "get_billing_status",
82
+ ]);
83
+
84
+ /** Human-readable description of a write tool call for the confirmation dialog. */
85
+ function describeToolCall(name: string, args: Record<string, any>): string {
86
+ const a = { ...args };
87
+ // Convert cents to dollars for display
88
+ for (const k of ["amount_cents", "principal_amount_cents", "total_amount_cents", "valuation_cap_cents"]) {
89
+ if (k in a) {
90
+ try { a._amount = centsToUsd(Number(a[k])); } catch { a._amount = String(a[k]); }
91
+ }
92
+ }
93
+ a._amount ??= "?";
94
+ a.institution_name ??= "Mercury";
95
+ a.payment_method ??= "ach";
96
+
97
+ const fmts: Record<string, string> = {
98
+ form_entity: 'Form a new {entity_type} named "{entity_name}" in {jurisdiction}',
99
+ convert_entity: "Convert entity to {new_entity_type}",
100
+ dissolve_entity: "Dissolve entity — {dissolution_reason}",
101
+ issue_equity: "Issue {shares} {grant_type} shares to {recipient_name}",
102
+ transfer_shares: "Transfer {shares} shares to {to_recipient_name}",
103
+ issue_safe: "Issue SAFE note to {investor_name} for {_amount}",
104
+ calculate_distribution: "Calculate {distribution_type} distribution of {_amount}",
105
+ create_invoice: "Create invoice for {customer_name} — {_amount}",
106
+ run_payroll: "Run payroll for {pay_period_start} to {pay_period_end}",
107
+ submit_payment: "Submit {_amount} payment to {recipient} via {payment_method}",
108
+ open_bank_account: "Open bank account at {institution_name}",
109
+ reconcile_ledger: "Reconcile ledger from {start_date} to {end_date}",
110
+ generate_contract: "Generate {template_type} contract for {counterparty_name}",
111
+ file_tax_document: "File {document_type} for tax year {tax_year}",
112
+ track_deadline: "Track {deadline_type} deadline — {description}",
113
+ classify_contractor: "Classify contractor {contractor_name} in {state}",
114
+ convene_meeting: "Convene {meeting_type} meeting",
115
+ cast_vote: "Cast {vote} vote",
116
+ schedule_meeting: "Schedule {meeting_type} meeting: {title}",
117
+ update_checklist: "Update workspace checklist",
118
+ create_agent: 'Create agent "{name}"',
119
+ send_agent_message: "Send message to agent",
120
+ update_agent: "Update agent configuration",
121
+ add_agent_skill: 'Add skill "{skill_name}" to agent',
122
+ };
123
+
124
+ const fmt = fmts[name];
125
+ if (fmt) {
126
+ try {
127
+ return fmt.replace(/\{(\w+)\}/g, (_, k) => String(a[k] ?? "?"));
128
+ } catch { /* fall through */ }
129
+ }
130
+ return name.replace(/_/g, " ");
131
+ }
132
+
133
+ // ── Tool Definitions ────────────────────────────────────────────
134
+
135
+ // Shared render helpers
136
+ function renderCallText(label: string, lines: string[], theme: any): Text {
137
+ let text = theme.fg("toolTitle", theme.bold(label + " "));
138
+ text += lines.map((l) => theme.fg("muted", l)).join(theme.fg("dim", " | "));
139
+ return new Text(text, 0, 0);
140
+ }
141
+
142
+ function renderResultOk(headline: string, details: string[], theme: any): Text {
143
+ let text = theme.fg("success", "✓ " + headline);
144
+ for (const d of details) text += "\n " + theme.fg("dim", d);
145
+ return new Text(text, 0, 0);
146
+ }
147
+
148
+ function renderResultErr(result: any, theme: any): Text | null {
149
+ const content = result?.content?.[0]?.text ?? result?.details?.error;
150
+ if (!content) return null;
151
+ try {
152
+ const d = typeof content === "string" ? JSON.parse(content) : content;
153
+ if (d?.error) return new Text(theme.fg("error", "✗ " + d.error), 0, 0);
154
+ } catch { /* not JSON */ }
155
+ return null;
156
+ }
157
+
158
+ function parseResult(result: any): any {
159
+ try {
160
+ const raw = result?.content?.[0]?.text ?? result?.details;
161
+ return typeof raw === "string" ? JSON.parse(raw) : raw ?? {};
162
+ } catch { return {}; }
163
+ }
164
+
165
+ // -- Read tools --
166
+
167
+ const getWorkspaceStatus = {
168
+ name: "get_workspace_status",
169
+ label: "Get Workspace Status",
170
+ description: "Get a summary of the current workspace: counts of entities, documents, grants, etc.",
171
+ parameters: Type.Object({}),
172
+ async execute(toolCallId: string, params: any, signal: any) {
173
+ const data = await apiCall("GET", `/v1/workspaces/${wsId()}/status`);
174
+ return { content: [{ type: "text" as const, text: JSON.stringify(data) }], details: data };
175
+ },
176
+ renderCall(args: any, theme: any) {
177
+ return renderCallText("workspace_status", ["Fetching summary"], theme);
178
+ },
179
+ renderResult(result: any, opts: any, theme: any) {
180
+ const err = renderResultErr(result, theme);
181
+ if (err) return err;
182
+ const d = parseResult(result);
183
+ return renderResultOk("Workspace Status", [
184
+ `Entities: ${d.entity_count ?? d.entities ?? "?"}`,
185
+ `Documents: ${d.document_count ?? d.documents ?? "?"}`,
186
+ `Grants: ${d.grant_count ?? d.grants ?? "?"}`,
187
+ ], theme);
188
+ },
189
+ };
190
+
191
+ const listEntities = {
192
+ name: "list_entities",
193
+ label: "List Entities",
194
+ description: "List all entities (companies) in the workspace. Optionally filter by type.",
195
+ parameters: Type.Object({
196
+ entity_type: Type.Optional(Type.String({ description: "Filter by type: 'llc', 'corporation', or empty for all" })),
197
+ }),
198
+ async execute(toolCallId: string, params: any, signal: any) {
199
+ const data = await apiCall("GET", `/v1/workspaces/${wsId()}/entities`);
200
+ return { content: [{ type: "text" as const, text: JSON.stringify(data) }], details: data };
201
+ },
202
+ renderCall(args: any, theme: any) {
203
+ const filter = args.entity_type ? `type=${args.entity_type}` : "all";
204
+ return renderCallText("list_entities", [filter], theme);
205
+ },
206
+ renderResult(result: any, opts: any, theme: any) {
207
+ const err = renderResultErr(result, theme);
208
+ if (err) return err;
209
+ const d = parseResult(result);
210
+ const entities = Array.isArray(d) ? d : d.entities ?? [];
211
+ const lines = entities.slice(0, 10).map((e: any) =>
212
+ `${e.legal_name || e.entity_name || e.name} (${e.entity_type}) — ${e.jurisdiction ?? ""} [${e.status ?? "active"}]`
213
+ );
214
+ if (entities.length > 10) lines.push(`... and ${entities.length - 10} more`);
215
+ return renderResultOk(`${entities.length} entities`, lines, theme);
216
+ },
217
+ };
218
+
219
+ const getCapTable = {
220
+ name: "get_cap_table",
221
+ label: "Get Cap Table",
222
+ description: "Get the full cap table for an entity including grants and SAFE notes.",
223
+ parameters: Type.Object({
224
+ entity_id: Type.String(),
225
+ }),
226
+ async execute(toolCallId: string, params: any, signal: any) {
227
+ const data = await apiCall("GET", `/v1/entities/${params.entity_id}/cap-table`);
228
+ return { content: [{ type: "text" as const, text: JSON.stringify(data) }], details: data };
229
+ },
230
+ renderCall(args: any, theme: any) {
231
+ return renderCallText("get_cap_table", [`entity=${args.entity_id}`], theme);
232
+ },
233
+ renderResult(result: any, opts: any, theme: any) {
234
+ const err = renderResultErr(result, theme);
235
+ if (err) return err;
236
+ const d = parseResult(result);
237
+ const grants = d.grants ?? d.rows ?? [];
238
+ const lines = grants.slice(0, 10).map((g: any) =>
239
+ `${g.holder_name ?? g.recipient_name ?? "?"}: ${g.shares ?? "?"} ${g.grant_type ?? ""} shares (${g.percentage != null ? (g.percentage * 100).toFixed(1) + "%" : "?"})`
240
+ );
241
+ return renderResultOk("Cap Table", lines, theme);
242
+ },
243
+ };
244
+
245
+ const listDocuments = {
246
+ name: "list_documents",
247
+ label: "List Documents",
248
+ description: "List all documents for an entity.",
249
+ parameters: Type.Object({
250
+ entity_id: Type.String(),
251
+ }),
252
+ async execute(toolCallId: string, params: any, signal: any) {
253
+ const data = await apiCall("GET", `/v1/formations/${params.entity_id}/documents`);
254
+ return { content: [{ type: "text" as const, text: JSON.stringify(data) }], details: data };
255
+ },
256
+ renderCall(args: any, theme: any) {
257
+ return renderCallText("list_documents", [`entity=${args.entity_id}`], theme);
258
+ },
259
+ renderResult(result: any, opts: any, theme: any) {
260
+ const err = renderResultErr(result, theme);
261
+ if (err) return err;
262
+ const d = parseResult(result);
263
+ const docs = Array.isArray(d) ? d : d.documents ?? [];
264
+ const statusBadge = (s: string) => {
265
+ if (s === "signed") return "✓signed";
266
+ if (s === "pending" || s === "pending_signature") return "⏳pending";
267
+ return s || "draft";
268
+ };
269
+ const lines = docs.slice(0, 10).map((doc: any) =>
270
+ `${doc.title ?? doc.document_type ?? "doc"} [${statusBadge(doc.status)}]`
271
+ );
272
+ return renderResultOk(`${docs.length} documents`, lines, theme);
273
+ },
274
+ };
275
+
276
+ const listSafeNotes = {
277
+ name: "list_safe_notes",
278
+ label: "List SAFE Notes",
279
+ description: "List SAFE notes for an entity.",
280
+ parameters: Type.Object({
281
+ entity_id: Type.String(),
282
+ }),
283
+ async execute(toolCallId: string, params: any, signal: any) {
284
+ const data = await apiCall("GET", `/v1/entities/${params.entity_id}/safe-notes`);
285
+ return { content: [{ type: "text" as const, text: JSON.stringify(data) }], details: data };
286
+ },
287
+ renderCall(args: any, theme: any) {
288
+ return renderCallText("list_safe_notes", [`entity=${args.entity_id}`], theme);
289
+ },
290
+ renderResult(result: any, opts: any, theme: any) {
291
+ const err = renderResultErr(result, theme);
292
+ if (err) return err;
293
+ const d = parseResult(result);
294
+ const notes = Array.isArray(d) ? d : d.safe_notes ?? [];
295
+ const lines = notes.map((n: any) =>
296
+ `${n.investor_name ?? "?"}: ${centsToUsd(n.principal_amount_cents ?? 0)} (cap ${centsToUsd(n.valuation_cap_cents ?? 0)}) ${n.safe_type ?? ""}`
297
+ );
298
+ return renderResultOk(`${notes.length} SAFE notes`, lines, theme);
299
+ },
300
+ };
301
+
302
+ const listAgents = {
303
+ name: "list_agents",
304
+ label: "List Agents",
305
+ description: "List all AI agents in the workspace.",
306
+ parameters: Type.Object({}),
307
+ async execute(toolCallId: string, params: any, signal: any) {
308
+ const data = await apiCall("GET", "/v1/agents");
309
+ return { content: [{ type: "text" as const, text: JSON.stringify(data) }], details: data };
310
+ },
311
+ renderCall(args: any, theme: any) {
312
+ return renderCallText("list_agents", ["Fetching agents"], theme);
313
+ },
314
+ renderResult(result: any, opts: any, theme: any) {
315
+ const err = renderResultErr(result, theme);
316
+ if (err) return err;
317
+ const d = parseResult(result);
318
+ const agents = Array.isArray(d) ? d : d.agents ?? [];
319
+ const lines = agents.map((a: any) => `${a.name} [${a.status ?? "active"}]`);
320
+ return renderResultOk(`${agents.length} agents`, lines, theme);
321
+ },
322
+ };
323
+
324
+ const getChecklist = {
325
+ name: "get_checklist",
326
+ label: "Get Checklist",
327
+ description: "Get the current workspace checklist.",
328
+ parameters: Type.Object({}),
329
+ async execute(toolCallId: string, params: any, signal: any) {
330
+ const checklistPath = join(process.env.CORP_CONFIG_DIR || join(homedir(), ".corp"), "checklist.md");
331
+ if (existsSync(checklistPath)) {
332
+ const checklist = readFileSync(checklistPath, "utf-8");
333
+ return { content: [{ type: "text" as const, text: JSON.stringify({ checklist }) }], details: { checklist } };
334
+ }
335
+ return { content: [{ type: "text" as const, text: JSON.stringify({ checklist: null, message: "No checklist yet." }) }], details: {} };
336
+ },
337
+ renderCall(args: any, theme: any) {
338
+ return renderCallText("get_checklist", ["Reading checklist"], theme);
339
+ },
340
+ renderResult(result: any, opts: any, theme: any) {
341
+ const d = parseResult(result);
342
+ if (!d.checklist) return new Text(theme.fg("muted", "No checklist yet"), 0, 0);
343
+ return renderResultOk("Checklist", d.checklist.split("\n").slice(0, 15), theme);
344
+ },
345
+ };
346
+
347
+ const getDocumentLink = {
348
+ name: "get_document_link",
349
+ label: "Get Document Link",
350
+ description: "Get a temporary download link (PDF) for a signed document. The link expires in 24 hours.",
351
+ parameters: Type.Object({
352
+ document_id: Type.String({ description: "The document ID to generate a download link for" }),
353
+ }),
354
+ async execute(toolCallId: string, params: any, signal: any) {
355
+ const cfg = loadConfig();
356
+ try {
357
+ const data = await apiCall("POST", `/v1/documents/${params.document_id}/request-copy`, { email: "owner@workspace" });
358
+ let url = data.download_url ?? "";
359
+ if (url.startsWith("/")) url = cfg.api_url.replace(/\/+$/, "") + url;
360
+ const result = { document_id: params.document_id, download_url: url, expires_in: "24 hours" };
361
+ return { content: [{ type: "text" as const, text: JSON.stringify(result) }], details: result };
362
+ } catch {
363
+ const result = { document_id: params.document_id, download_url: `${cfg.api_url.replace(/\/+$/, "")}/v1/documents/${params.document_id}/pdf`, note: "Use your API key to authenticate the download." };
364
+ return { content: [{ type: "text" as const, text: JSON.stringify(result) }], details: result };
365
+ }
366
+ },
367
+ renderCall(args: any, theme: any) {
368
+ return renderCallText("get_document_link", [`doc=${args.document_id}`], theme);
369
+ },
370
+ renderResult(result: any, opts: any, theme: any) {
371
+ const d = parseResult(result);
372
+ return renderResultOk("Document Link", [d.download_url ?? "?"], theme);
373
+ },
374
+ };
375
+
376
+ const getSigningLink = {
377
+ name: "get_signing_link",
378
+ label: "Get Signing Link",
379
+ description: "Generate a signing link for a document. Returns a URL the user must open to sign. Documents can ONLY be signed through this link.",
380
+ parameters: Type.Object({
381
+ document_id: Type.String({ description: "The document ID to generate a signing link for" }),
382
+ }),
383
+ async execute(toolCallId: string, params: any, signal: any) {
384
+ const result = {
385
+ document_id: params.document_id,
386
+ signing_url: `https://humans.thecorporation.ai/sign/${params.document_id}`,
387
+ };
388
+ return { content: [{ type: "text" as const, text: JSON.stringify(result) }], details: result };
389
+ },
390
+ renderCall(args: any, theme: any) {
391
+ return renderCallText("get_signing_link", [`doc=${args.document_id}`], theme);
392
+ },
393
+ renderResult(result: any, opts: any, theme: any) {
394
+ const d = parseResult(result);
395
+ return renderResultOk("Signing Link", [d.signing_url ?? "?"], theme);
396
+ },
397
+ };
398
+
399
+ const listObligations = {
400
+ name: "list_obligations",
401
+ label: "List Obligations",
402
+ description: "List obligations with urgency tiers. Optionally filter by tier.",
403
+ parameters: Type.Object({
404
+ tier: Type.Optional(Type.String({ description: "Filter: overdue, due_today, d1, d7, d14, d30, upcoming" })),
405
+ }),
406
+ async execute(toolCallId: string, params: any, signal: any) {
407
+ const q = params.tier ? `?tier=${params.tier}` : "";
408
+ const data = await apiCall("GET", `/v1/obligations/summary${q}`);
409
+ return { content: [{ type: "text" as const, text: JSON.stringify(data) }], details: data };
410
+ },
411
+ renderCall(args: any, theme: any) {
412
+ return renderCallText("list_obligations", [args.tier ?? "all tiers"], theme);
413
+ },
414
+ renderResult(result: any, opts: any, theme: any) {
415
+ const err = renderResultErr(result, theme);
416
+ if (err) return err;
417
+ const d = parseResult(result);
418
+ const items = d.obligations ?? d.items ?? [];
419
+ const tierColor = (t: string) => {
420
+ if (t === "overdue") return "error";
421
+ if (t === "due_today" || t === "d1") return "warning";
422
+ return "dim";
423
+ };
424
+ const lines = items.slice(0, 10).map((o: any) =>
425
+ theme.fg(tierColor(o.tier ?? ""), `[${o.tier ?? "?"}] ${o.description ?? o.title ?? "obligation"}`)
426
+ );
427
+ return renderResultOk(`${items.length} obligations`, lines, theme);
428
+ },
429
+ };
430
+
431
+ const getBillingStatus = {
432
+ name: "get_billing_status",
433
+ label: "Get Billing Status",
434
+ description: "Get current billing status: tier, subscriptions, usage, and available plans.",
435
+ parameters: Type.Object({}),
436
+ async execute(toolCallId: string, params: any, signal: any) {
437
+ const [status, plans] = await Promise.all([
438
+ apiCall("GET", `/v1/billing/status?workspace_id=${wsId()}`),
439
+ apiCall("GET", "/v1/billing/plans"),
440
+ ]);
441
+ const result = { status, plans: plans?.plans ?? plans };
442
+ return { content: [{ type: "text" as const, text: JSON.stringify(result) }], details: result };
443
+ },
444
+ renderCall(args: any, theme: any) {
445
+ return renderCallText("billing_status", ["Fetching billing info"], theme);
446
+ },
447
+ renderResult(result: any, opts: any, theme: any) {
448
+ const d = parseResult(result);
449
+ const s = d.status ?? {};
450
+ return renderResultOk("Billing", [
451
+ `Tier: ${s.tier ?? s.plan ?? "?"}`,
452
+ `Tool calls: ${s.tool_call_count ?? "?"}/${s.tool_call_limit ?? "?"}`,
453
+ ], theme);
454
+ },
455
+ };
456
+
457
+ // -- Entity lifecycle tools --
458
+
459
+ const formEntity = {
460
+ name: "form_entity",
461
+ label: "Form Entity",
462
+ description: "Form a new business entity. LLC uses US-WY, corporation uses US-DE. Collect member details and ownership allocations before calling.",
463
+ parameters: Type.Object({
464
+ entity_type: Type.String({ description: "Must be 'llc' or 'corporation'" }),
465
+ entity_name: Type.String({ description: "Legal name of the entity" }),
466
+ jurisdiction: Type.String({ description: "Jurisdiction code (e.g. US-WY for LLC, US-DE for corporation)" }),
467
+ members: Type.Array(Type.Object({
468
+ name: Type.String({ description: "Member's full name" }),
469
+ email: Type.String({ description: "Member's email" }),
470
+ role: Type.String({ description: "Role (e.g. Member, Manager, Incorporator)" }),
471
+ investor_type: Type.Optional(Type.String({ description: "natural_person or entity" })),
472
+ ownership_pct: Type.Optional(Type.Number({ description: "Ownership fraction 0.0-1.0 (for LLCs). Must total 1.0." })),
473
+ share_count: Type.Optional(Type.Integer({ description: "Number of shares (for corporations)" })),
474
+ }), { description: "Founding members with ownership allocations" }),
475
+ }),
476
+ async execute(toolCallId: string, params: any, signal: any) {
477
+ const body = {
478
+ entity_type: params.entity_type,
479
+ legal_name: params.entity_name,
480
+ jurisdiction: params.jurisdiction,
481
+ members: params.members,
482
+ workspace_id: wsId(),
483
+ };
484
+ const data = await apiCall("POST", "/v1/formations", body);
485
+ return { content: [{ type: "text" as const, text: JSON.stringify(data) }], details: data };
486
+ },
487
+ renderCall(args: any, theme: any) {
488
+ return renderCallText("form_entity", [
489
+ args.entity_name,
490
+ `${args.entity_type} in ${args.jurisdiction}`,
491
+ `${args.members?.length ?? 0} members`,
492
+ ], theme);
493
+ },
494
+ renderResult(result: any, opts: any, theme: any) {
495
+ const err = renderResultErr(result, theme);
496
+ if (err) return err;
497
+ const d = parseResult(result);
498
+ return renderResultOk(`Entity formed: ${d.legal_name ?? d.entity_name ?? "?"}`, [
499
+ `ID: ${d.entity_id ?? d.id ?? "?"}`,
500
+ `Documents: ${d.documents?.length ?? 0} generated`,
501
+ ], theme);
502
+ },
503
+ };
504
+
505
+ const convertEntity = {
506
+ name: "convert_entity",
507
+ label: "Convert Entity",
508
+ description: "Convert an entity from one type to another (e.g. LLC to C-Corp).",
509
+ parameters: Type.Object({
510
+ entity_id: Type.String(),
511
+ new_entity_type: Type.String({ description: "'llc' or 'corporation'" }),
512
+ new_jurisdiction: Type.Optional(Type.String({ description: "New jurisdiction (defaults to current)" })),
513
+ }),
514
+ async execute(toolCallId: string, params: any, signal: any) {
515
+ const body: any = { new_entity_type: params.new_entity_type };
516
+ if (params.new_jurisdiction) body.new_jurisdiction = params.new_jurisdiction;
517
+ const data = await apiCall("POST", `/v1/entities/${params.entity_id}/convert`, body);
518
+ return { content: [{ type: "text" as const, text: JSON.stringify(data) }], details: data };
519
+ },
520
+ renderCall(args: any, theme: any) {
521
+ return renderCallText("convert_entity", [`→ ${args.new_entity_type}`, args.entity_id], theme);
522
+ },
523
+ renderResult(result: any, opts: any, theme: any) {
524
+ const err = renderResultErr(result, theme);
525
+ if (err) return err;
526
+ const d = parseResult(result);
527
+ return renderResultOk("Entity converted", [`New type: ${d.entity_type ?? d.new_entity_type ?? "?"}`], theme);
528
+ },
529
+ };
530
+
531
+ const dissolveEntity = {
532
+ name: "dissolve_entity",
533
+ label: "Dissolve Entity",
534
+ description: "Initiate entity dissolution and wind-down workflow.",
535
+ parameters: Type.Object({
536
+ entity_id: Type.String(),
537
+ dissolution_reason: Type.String(),
538
+ effective_date: Type.Optional(Type.String({ description: "ISO 8601 date (defaults to today)" })),
539
+ }),
540
+ async execute(toolCallId: string, params: any, signal: any) {
541
+ const body: any = { dissolution_reason: params.dissolution_reason };
542
+ if (params.effective_date) body.effective_date = params.effective_date;
543
+ const data = await apiCall("POST", `/v1/entities/${params.entity_id}/dissolve`, body);
544
+ return { content: [{ type: "text" as const, text: JSON.stringify(data) }], details: data };
545
+ },
546
+ renderCall(args: any, theme: any) {
547
+ return renderCallText("dissolve_entity", [args.entity_id, args.dissolution_reason], theme);
548
+ },
549
+ renderResult(result: any, opts: any, theme: any) {
550
+ const err = renderResultErr(result, theme);
551
+ if (err) return err;
552
+ return renderResultOk("Entity dissolution initiated", [], theme);
553
+ },
554
+ };
555
+
556
+ // -- Equity tools --
557
+
558
+ const issueEquity = {
559
+ name: "issue_equity",
560
+ label: "Issue Equity",
561
+ description: "Issue equity (shares or membership units) to a recipient.",
562
+ parameters: Type.Object({
563
+ entity_id: Type.String(),
564
+ grant_type: Type.String({ description: "'common', 'preferred', 'option', or 'unit'" }),
565
+ shares: Type.Integer(),
566
+ recipient_name: Type.String(),
567
+ }),
568
+ async execute(toolCallId: string, params: any, signal: any) {
569
+ const data = await apiCall("POST", "/v1/equity/grants", params);
570
+ return { content: [{ type: "text" as const, text: JSON.stringify(data) }], details: data };
571
+ },
572
+ renderCall(args: any, theme: any) {
573
+ return renderCallText("issue_equity", [
574
+ `${args.shares} ${args.grant_type}`,
575
+ `→ ${args.recipient_name}`,
576
+ ], theme);
577
+ },
578
+ renderResult(result: any, opts: any, theme: any) {
579
+ const err = renderResultErr(result, theme);
580
+ if (err) return err;
581
+ const d = parseResult(result);
582
+ return renderResultOk(`Equity issued: ${d.shares ?? "?"} ${d.grant_type ?? ""} shares`, [
583
+ `Recipient: ${d.recipient_name ?? "?"}`,
584
+ `Grant ID: ${d.grant_id ?? d.id ?? "?"}`,
585
+ ], theme);
586
+ },
587
+ };
588
+
589
+ const transferShares = {
590
+ name: "transfer_shares",
591
+ label: "Transfer Shares",
592
+ description: "Transfer shares from one grant to a new recipient.",
593
+ parameters: Type.Object({
594
+ entity_id: Type.String(),
595
+ from_grant_id: Type.String(),
596
+ to_recipient_name: Type.String(),
597
+ shares: Type.Integer(),
598
+ transfer_type: Type.Optional(Type.String({ description: "'sale', 'gift', or 'rofr_exercise'" })),
599
+ }),
600
+ async execute(toolCallId: string, params: any, signal: any) {
601
+ const data = await apiCall("POST", "/v1/share-transfers", params);
602
+ return { content: [{ type: "text" as const, text: JSON.stringify(data) }], details: data };
603
+ },
604
+ renderCall(args: any, theme: any) {
605
+ return renderCallText("transfer_shares", [
606
+ `${args.shares} shares`,
607
+ `→ ${args.to_recipient_name}`,
608
+ args.transfer_type ?? "transfer",
609
+ ], theme);
610
+ },
611
+ renderResult(result: any, opts: any, theme: any) {
612
+ const err = renderResultErr(result, theme);
613
+ if (err) return err;
614
+ return renderResultOk("Shares transferred", [`${parseResult(result).shares ?? "?"} shares`], theme);
615
+ },
616
+ };
617
+
618
+ const issueSafe = {
619
+ name: "issue_safe",
620
+ label: "Issue SAFE",
621
+ description: "Issue a SAFE note to an investor.",
622
+ parameters: Type.Object({
623
+ entity_id: Type.String(),
624
+ investor_name: Type.String(),
625
+ principal_amount_cents: Type.Integer({ description: "Investment amount in cents" }),
626
+ valuation_cap_cents: Type.Integer({ description: "Valuation cap in cents" }),
627
+ safe_type: Type.Optional(Type.String({ description: "'pre_money', 'post_money', or 'mfn'" })),
628
+ }),
629
+ async execute(toolCallId: string, params: any, signal: any) {
630
+ const data = await apiCall("POST", "/v1/safe-notes", params);
631
+ return { content: [{ type: "text" as const, text: JSON.stringify(data) }], details: data };
632
+ },
633
+ renderCall(args: any, theme: any) {
634
+ return renderCallText("issue_safe", [
635
+ args.investor_name,
636
+ centsToUsd(args.principal_amount_cents),
637
+ `cap ${centsToUsd(args.valuation_cap_cents)}`,
638
+ ], theme);
639
+ },
640
+ renderResult(result: any, opts: any, theme: any) {
641
+ const err = renderResultErr(result, theme);
642
+ if (err) return err;
643
+ const d = parseResult(result);
644
+ return renderResultOk(`SAFE issued to ${d.investor_name ?? "?"}`, [
645
+ `Amount: ${centsToUsd(d.principal_amount_cents ?? 0)}`,
646
+ `Cap: ${centsToUsd(d.valuation_cap_cents ?? 0)}`,
647
+ ], theme);
648
+ },
649
+ };
650
+
651
+ const calculateDistribution = {
652
+ name: "calculate_distribution",
653
+ label: "Calculate Distribution",
654
+ description: "Calculate and record a distribution to equity holders.",
655
+ parameters: Type.Object({
656
+ entity_id: Type.String(),
657
+ total_amount_cents: Type.Integer(),
658
+ distribution_type: Type.String({ description: "'pro_rata', 'waterfall', or 'guaranteed_payment'" }),
659
+ }),
660
+ async execute(toolCallId: string, params: any, signal: any) {
661
+ const data = await apiCall("POST", "/v1/distributions", params);
662
+ return { content: [{ type: "text" as const, text: JSON.stringify(data) }], details: data };
663
+ },
664
+ renderCall(args: any, theme: any) {
665
+ return renderCallText("calculate_distribution", [
666
+ args.distribution_type,
667
+ centsToUsd(args.total_amount_cents),
668
+ ], theme);
669
+ },
670
+ renderResult(result: any, opts: any, theme: any) {
671
+ const err = renderResultErr(result, theme);
672
+ if (err) return err;
673
+ const d = parseResult(result);
674
+ return renderResultOk(`Distribution: ${centsToUsd(d.total_amount_cents ?? 0)}`, [
675
+ `Type: ${d.distribution_type ?? "?"}`,
676
+ ], theme);
677
+ },
678
+ };
679
+
680
+ // -- Finance tools --
681
+
682
+ const createInvoice = {
683
+ name: "create_invoice",
684
+ label: "Create Invoice",
685
+ description: "Create an invoice for a customer.",
686
+ parameters: Type.Object({
687
+ entity_id: Type.String(),
688
+ customer_name: Type.String(),
689
+ amount_cents: Type.Integer(),
690
+ due_date: Type.String({ description: "ISO 8601 date" }),
691
+ description: Type.Optional(Type.String()),
692
+ }),
693
+ async execute(toolCallId: string, params: any, signal: any) {
694
+ const data = await apiCall("POST", "/v1/invoices", params);
695
+ return { content: [{ type: "text" as const, text: JSON.stringify(data) }], details: data };
696
+ },
697
+ renderCall(args: any, theme: any) {
698
+ return renderCallText("create_invoice", [
699
+ args.customer_name,
700
+ centsToUsd(args.amount_cents),
701
+ `due ${args.due_date}`,
702
+ ], theme);
703
+ },
704
+ renderResult(result: any, opts: any, theme: any) {
705
+ const err = renderResultErr(result, theme);
706
+ if (err) return err;
707
+ const d = parseResult(result);
708
+ return renderResultOk(`Invoice created: ${centsToUsd(d.amount_cents ?? 0)}`, [
709
+ `Customer: ${d.customer_name ?? "?"}`,
710
+ `Due: ${d.due_date ?? "?"}`,
711
+ ], theme);
712
+ },
713
+ };
714
+
715
+ const runPayroll = {
716
+ name: "run_payroll",
717
+ label: "Run Payroll",
718
+ description: "Run payroll for an entity over a pay period.",
719
+ parameters: Type.Object({
720
+ entity_id: Type.String(),
721
+ pay_period_start: Type.String(),
722
+ pay_period_end: Type.String(),
723
+ }),
724
+ async execute(toolCallId: string, params: any, signal: any) {
725
+ const data = await apiCall("POST", "/v1/payroll/runs", params);
726
+ return { content: [{ type: "text" as const, text: JSON.stringify(data) }], details: data };
727
+ },
728
+ renderCall(args: any, theme: any) {
729
+ return renderCallText("run_payroll", [`${args.pay_period_start} → ${args.pay_period_end}`], theme);
730
+ },
731
+ renderResult(result: any, opts: any, theme: any) {
732
+ const err = renderResultErr(result, theme);
733
+ if (err) return err;
734
+ return renderResultOk("Payroll run completed", [], theme);
735
+ },
736
+ };
737
+
738
+ const submitPayment = {
739
+ name: "submit_payment",
740
+ label: "Submit Payment",
741
+ description: "Submit a payment from an entity to a recipient.",
742
+ parameters: Type.Object({
743
+ entity_id: Type.String(),
744
+ amount_cents: Type.Integer(),
745
+ recipient: Type.String(),
746
+ payment_method: Type.Optional(Type.String({ description: "'ach', 'wire', or 'check'" })),
747
+ }),
748
+ async execute(toolCallId: string, params: any, signal: any) {
749
+ const data = await apiCall("POST", "/v1/payments", params);
750
+ return { content: [{ type: "text" as const, text: JSON.stringify(data) }], details: data };
751
+ },
752
+ renderCall(args: any, theme: any) {
753
+ return renderCallText("submit_payment", [
754
+ centsToUsd(args.amount_cents),
755
+ `→ ${args.recipient}`,
756
+ args.payment_method ?? "ach",
757
+ ], theme);
758
+ },
759
+ renderResult(result: any, opts: any, theme: any) {
760
+ const err = renderResultErr(result, theme);
761
+ if (err) return err;
762
+ const d = parseResult(result);
763
+ return renderResultOk(`Payment: ${centsToUsd(d.amount_cents ?? 0)}`, [
764
+ `Recipient: ${d.recipient ?? "?"}`,
765
+ `Method: ${d.payment_method ?? "ach"}`,
766
+ ], theme);
767
+ },
768
+ };
769
+
770
+ const openBankAccount = {
771
+ name: "open_bank_account",
772
+ label: "Open Bank Account",
773
+ description: "Open a business bank account for an entity.",
774
+ parameters: Type.Object({
775
+ entity_id: Type.String(),
776
+ institution_name: Type.Optional(Type.String({ description: "Bank name (defaults to Mercury)" })),
777
+ }),
778
+ async execute(toolCallId: string, params: any, signal: any) {
779
+ const body: any = { entity_id: params.entity_id };
780
+ if (params.institution_name) body.institution_name = params.institution_name;
781
+ const data = await apiCall("POST", "/v1/bank-accounts", body);
782
+ return { content: [{ type: "text" as const, text: JSON.stringify(data) }], details: data };
783
+ },
784
+ renderCall(args: any, theme: any) {
785
+ return renderCallText("open_bank_account", [args.institution_name ?? "Mercury"], theme);
786
+ },
787
+ renderResult(result: any, opts: any, theme: any) {
788
+ const err = renderResultErr(result, theme);
789
+ if (err) return err;
790
+ const d = parseResult(result);
791
+ return renderResultOk("Bank account opened", [
792
+ `Institution: ${d.institution_name ?? "Mercury"}`,
793
+ ], theme);
794
+ },
795
+ };
796
+
797
+ const reconcileLedger = {
798
+ name: "reconcile_ledger",
799
+ label: "Reconcile Ledger",
800
+ description: "Reconcile an entity's ledger for a given period.",
801
+ parameters: Type.Object({
802
+ entity_id: Type.String(),
803
+ start_date: Type.String(),
804
+ end_date: Type.String(),
805
+ }),
806
+ async execute(toolCallId: string, params: any, signal: any) {
807
+ const data = await apiCall("POST", "/v1/ledger/reconcile", params);
808
+ return { content: [{ type: "text" as const, text: JSON.stringify(data) }], details: data };
809
+ },
810
+ renderCall(args: any, theme: any) {
811
+ return renderCallText("reconcile_ledger", [`${args.start_date} → ${args.end_date}`], theme);
812
+ },
813
+ renderResult(result: any, opts: any, theme: any) {
814
+ const err = renderResultErr(result, theme);
815
+ if (err) return err;
816
+ return renderResultOk("Ledger reconciled", [], theme);
817
+ },
818
+ };
819
+
820
+ // -- Documents & compliance tools --
821
+
822
+ const generateContract = {
823
+ name: "generate_contract",
824
+ label: "Generate Contract",
825
+ description: "Generate a contract from a template (NDA, contractor agreement, offer letter, etc).",
826
+ parameters: Type.Object({
827
+ entity_id: Type.String(),
828
+ template_type: Type.String({ description: "'nda', 'contractor_agreement', 'employment_offer', 'consulting_agreement'" }),
829
+ counterparty_name: Type.String(),
830
+ effective_date: Type.Optional(Type.String({ description: "ISO 8601 date (defaults to today)" })),
831
+ }),
832
+ async execute(toolCallId: string, params: any, signal: any) {
833
+ const data = await apiCall("POST", "/v1/contracts", params);
834
+ return { content: [{ type: "text" as const, text: JSON.stringify(data) }], details: data };
835
+ },
836
+ renderCall(args: any, theme: any) {
837
+ return renderCallText("generate_contract", [
838
+ args.template_type,
839
+ `for ${args.counterparty_name}`,
840
+ ], theme);
841
+ },
842
+ renderResult(result: any, opts: any, theme: any) {
843
+ const err = renderResultErr(result, theme);
844
+ if (err) return err;
845
+ const d = parseResult(result);
846
+ return renderResultOk(`Contract generated: ${d.template_type ?? "?"}`, [
847
+ `Counterparty: ${d.counterparty_name ?? "?"}`,
848
+ `Status: ${d.status ?? "draft"}`,
849
+ ], theme);
850
+ },
851
+ };
852
+
853
+ const fileTaxDocument = {
854
+ name: "file_tax_document",
855
+ label: "File Tax Document",
856
+ description: "Generate and file a tax document for an entity.",
857
+ parameters: Type.Object({
858
+ entity_id: Type.String(),
859
+ document_type: Type.String({ description: "'1099-NEC', 'K-1', '941', 'W-2', 'estimated_tax'" }),
860
+ tax_year: Type.Integer(),
861
+ }),
862
+ async execute(toolCallId: string, params: any, signal: any) {
863
+ const data = await apiCall("POST", "/v1/tax/filings", params);
864
+ return { content: [{ type: "text" as const, text: JSON.stringify(data) }], details: data };
865
+ },
866
+ renderCall(args: any, theme: any) {
867
+ return renderCallText("file_tax_document", [args.document_type, `TY ${args.tax_year}`], theme);
868
+ },
869
+ renderResult(result: any, opts: any, theme: any) {
870
+ const err = renderResultErr(result, theme);
871
+ if (err) return err;
872
+ const d = parseResult(result);
873
+ return renderResultOk(`Tax document filed: ${d.document_type ?? "?"}`, [
874
+ `Tax year: ${d.tax_year ?? "?"}`,
875
+ ], theme);
876
+ },
877
+ };
878
+
879
+ const trackDeadline = {
880
+ name: "track_deadline",
881
+ label: "Track Deadline",
882
+ description: "Track a compliance or filing deadline.",
883
+ parameters: Type.Object({
884
+ entity_id: Type.String(),
885
+ deadline_type: Type.String({ description: "'tax_filing', 'annual_report', 'sales_tax', 'franchise_tax'" }),
886
+ due_date: Type.String(),
887
+ description: Type.String(),
888
+ recurrence: Type.Optional(Type.String({ description: "'monthly', 'quarterly', 'annually', or empty" })),
889
+ }),
890
+ async execute(toolCallId: string, params: any, signal: any) {
891
+ const data = await apiCall("POST", "/v1/deadlines", params);
892
+ return { content: [{ type: "text" as const, text: JSON.stringify(data) }], details: data };
893
+ },
894
+ renderCall(args: any, theme: any) {
895
+ return renderCallText("track_deadline", [
896
+ args.deadline_type,
897
+ `due ${args.due_date}`,
898
+ args.recurrence ?? "",
899
+ ].filter(Boolean), theme);
900
+ },
901
+ renderResult(result: any, opts: any, theme: any) {
902
+ const err = renderResultErr(result, theme);
903
+ if (err) return err;
904
+ return renderResultOk("Deadline tracked", [
905
+ `${parseResult(result).deadline_type ?? "?"}: ${parseResult(result).due_date ?? "?"}`,
906
+ ], theme);
907
+ },
908
+ };
909
+
910
+ const classifyContractor = {
911
+ name: "classify_contractor",
912
+ label: "Classify Contractor",
913
+ description: "Analyze contractor classification risk (employee vs independent contractor).",
914
+ parameters: Type.Object({
915
+ entity_id: Type.String(),
916
+ contractor_name: Type.String(),
917
+ state: Type.String({ description: "US state code" }),
918
+ hours_per_week: Type.Integer(),
919
+ exclusive_client: Type.Boolean(),
920
+ duration_months: Type.Integer(),
921
+ provides_tools: Type.Optional(Type.Boolean()),
922
+ }),
923
+ async execute(toolCallId: string, params: any, signal: any) {
924
+ const data = await apiCall("POST", "/v1/contractors/classify", params);
925
+ return { content: [{ type: "text" as const, text: JSON.stringify(data) }], details: data };
926
+ },
927
+ renderCall(args: any, theme: any) {
928
+ return renderCallText("classify_contractor", [
929
+ args.contractor_name,
930
+ args.state,
931
+ `${args.hours_per_week}h/wk`,
932
+ ], theme);
933
+ },
934
+ renderResult(result: any, opts: any, theme: any) {
935
+ const err = renderResultErr(result, theme);
936
+ if (err) return err;
937
+ const d = parseResult(result);
938
+ const riskColor = (d.risk_level === "high" || d.classification === "employee") ? "error" : "success";
939
+ return new Text(
940
+ theme.fg(riskColor, `✓ ${d.contractor_name ?? "?"}: ${d.classification ?? d.risk_level ?? "?"}`) +
941
+ (d.recommendation ? "\n " + theme.fg("dim", d.recommendation) : ""),
942
+ 0, 0
943
+ );
944
+ },
945
+ };
946
+
947
+ // -- Governance tools --
948
+
949
+ const conveneMeeting = {
950
+ name: "convene_meeting",
951
+ label: "Convene Meeting",
952
+ description: "Convene a governance meeting (board, shareholder, or member vote).",
953
+ parameters: Type.Object({
954
+ entity_id: Type.String(),
955
+ meeting_type: Type.String({ description: "'board', 'shareholder', or 'member'" }),
956
+ agenda_items: Type.Array(Type.String()),
957
+ }),
958
+ async execute(toolCallId: string, params: any, signal: any) {
959
+ const data = await apiCall("POST", "/v1/meetings", params);
960
+ return { content: [{ type: "text" as const, text: JSON.stringify(data) }], details: data };
961
+ },
962
+ renderCall(args: any, theme: any) {
963
+ return renderCallText("convene_meeting", [
964
+ args.meeting_type,
965
+ `${args.agenda_items?.length ?? 0} agenda items`,
966
+ ], theme);
967
+ },
968
+ renderResult(result: any, opts: any, theme: any) {
969
+ const err = renderResultErr(result, theme);
970
+ if (err) return err;
971
+ const d = parseResult(result);
972
+ return renderResultOk(`Meeting convened: ${d.meeting_type ?? "?"}`, [
973
+ `Meeting ID: ${d.meeting_id ?? d.id ?? "?"}`,
974
+ ], theme);
975
+ },
976
+ };
977
+
978
+ const castVote = {
979
+ name: "cast_vote",
980
+ label: "Cast Vote",
981
+ description: "Cast a vote on an agenda item in a governance meeting.",
982
+ parameters: Type.Object({
983
+ meeting_id: Type.String(),
984
+ agenda_item_id: Type.String(),
985
+ voter_id: Type.String(),
986
+ vote: Type.String({ description: "'for', 'against', 'abstain', or 'recusal'" }),
987
+ }),
988
+ async execute(toolCallId: string, params: any, signal: any) {
989
+ const data = await apiCall(
990
+ "POST",
991
+ `/v1/meetings/${params.meeting_id}/agenda-items/${params.agenda_item_id}/vote`,
992
+ { voter_id: params.voter_id, vote_value: params.vote }
993
+ );
994
+ return { content: [{ type: "text" as const, text: JSON.stringify(data) }], details: data };
995
+ },
996
+ renderCall(args: any, theme: any) {
997
+ const voteColor = args.vote === "for" ? "success" : args.vote === "against" ? "error" : "warning";
998
+ return new Text(
999
+ theme.fg("toolTitle", theme.bold("cast_vote ")) +
1000
+ theme.fg(voteColor, args.vote) +
1001
+ theme.fg("muted", ` on ${args.agenda_item_id}`),
1002
+ 0, 0
1003
+ );
1004
+ },
1005
+ renderResult(result: any, opts: any, theme: any) {
1006
+ const err = renderResultErr(result, theme);
1007
+ if (err) return err;
1008
+ return renderResultOk("Vote recorded", [], theme);
1009
+ },
1010
+ };
1011
+
1012
+ const scheduleMeeting = {
1013
+ name: "schedule_meeting",
1014
+ label: "Schedule Meeting",
1015
+ description: "Schedule a board or member meeting.",
1016
+ parameters: Type.Object({
1017
+ body_id: Type.String({ description: "Governance body ID" }),
1018
+ meeting_type: Type.String({ description: "regular, special, annual, or written_consent" }),
1019
+ title: Type.String({ description: "Meeting title" }),
1020
+ proposed_date: Type.String({ description: "ISO datetime for the meeting" }),
1021
+ agenda_items: Type.Optional(Type.Array(Type.String(), { description: "Agenda item titles" })),
1022
+ }),
1023
+ async execute(toolCallId: string, params: any, signal: any) {
1024
+ const body: any = {
1025
+ body_id: params.body_id,
1026
+ meeting_type: params.meeting_type,
1027
+ title: params.title,
1028
+ proposed_date: params.proposed_date,
1029
+ };
1030
+ if (params.agenda_items) body.agenda_item_titles = params.agenda_items;
1031
+ const data = await apiCall("POST", "/v1/meetings", body);
1032
+ return { content: [{ type: "text" as const, text: JSON.stringify(data) }], details: data };
1033
+ },
1034
+ renderCall(args: any, theme: any) {
1035
+ return renderCallText("schedule_meeting", [args.title, args.meeting_type, args.proposed_date], theme);
1036
+ },
1037
+ renderResult(result: any, opts: any, theme: any) {
1038
+ const err = renderResultErr(result, theme);
1039
+ if (err) return err;
1040
+ const d = parseResult(result);
1041
+ return renderResultOk(`Meeting scheduled: ${d.title ?? "?"}`, [
1042
+ `Date: ${d.proposed_date ?? "?"}`,
1043
+ ], theme);
1044
+ },
1045
+ };
1046
+
1047
+ // -- Workspace tools --
1048
+
1049
+ const updateChecklist = {
1050
+ name: "update_checklist",
1051
+ label: "Update Checklist",
1052
+ description: "Create or replace the workspace checklist. Use markdown checkbox format.",
1053
+ parameters: Type.Object({
1054
+ checklist: Type.String({ description: "Full checklist in markdown checkbox format" }),
1055
+ }),
1056
+ async execute(toolCallId: string, params: any, signal: any) {
1057
+ const configDir = process.env.CORP_CONFIG_DIR || join(homedir(), ".corp");
1058
+ mkdirSync(configDir, { recursive: true });
1059
+ writeFileSync(join(configDir, "checklist.md"), params.checklist);
1060
+ return { content: [{ type: "text" as const, text: JSON.stringify({ status: "updated", checklist: params.checklist }) }], details: { status: "updated" } };
1061
+ },
1062
+ renderCall(args: any, theme: any) {
1063
+ const lineCount = args.checklist?.split("\n").length ?? 0;
1064
+ return renderCallText("update_checklist", [`${lineCount} lines`], theme);
1065
+ },
1066
+ renderResult(result: any, opts: any, theme: any) {
1067
+ return renderResultOk("Checklist updated", [], theme);
1068
+ },
1069
+ };
1070
+
1071
+ // -- Agent tools --
1072
+
1073
+ const createAgent = {
1074
+ name: "create_agent",
1075
+ label: "Create Agent",
1076
+ description: "Create a new autonomous AI agent for a corporate operations task. Requires paid plan.",
1077
+ parameters: Type.Object({
1078
+ name: Type.String(),
1079
+ system_prompt: Type.String(),
1080
+ model: Type.Optional(Type.String({ description: "LLM model (default: anthropic/claude-sonnet-4-6)" })),
1081
+ entity_id: Type.Optional(Type.String({ description: "Optional entity to bind the agent to" })),
1082
+ }),
1083
+ async execute(toolCallId: string, params: any, signal: any) {
1084
+ const data = await apiCall("POST", "/v1/agents", params);
1085
+ return { content: [{ type: "text" as const, text: JSON.stringify(data) }], details: data };
1086
+ },
1087
+ renderCall(args: any, theme: any) {
1088
+ return renderCallText("create_agent", [`"${args.name}"`], theme);
1089
+ },
1090
+ renderResult(result: any, opts: any, theme: any) {
1091
+ const err = renderResultErr(result, theme);
1092
+ if (err) return err;
1093
+ const d = parseResult(result);
1094
+ return renderResultOk(`Agent created: ${d.name ?? "?"}`, [
1095
+ `ID: ${d.agent_id ?? d.id ?? "?"}`,
1096
+ ], theme);
1097
+ },
1098
+ };
1099
+
1100
+ const sendAgentMessage = {
1101
+ name: "send_agent_message",
1102
+ label: "Send Agent Message",
1103
+ description: "Send a message to an agent, triggering an execution.",
1104
+ parameters: Type.Object({
1105
+ agent_id: Type.String(),
1106
+ message: Type.String(),
1107
+ }),
1108
+ async execute(toolCallId: string, params: any, signal: any) {
1109
+ const data = await apiCall("POST", `/v1/agents/${params.agent_id}/messages`, { body: params.message });
1110
+ return { content: [{ type: "text" as const, text: JSON.stringify(data) }], details: data };
1111
+ },
1112
+ renderCall(args: any, theme: any) {
1113
+ const preview = args.message?.length > 40 ? args.message.slice(0, 40) + "…" : args.message;
1114
+ return renderCallText("send_agent_message", [preview], theme);
1115
+ },
1116
+ renderResult(result: any, opts: any, theme: any) {
1117
+ const err = renderResultErr(result, theme);
1118
+ if (err) return err;
1119
+ return renderResultOk("Message sent to agent", [], theme);
1120
+ },
1121
+ };
1122
+
1123
+ const updateAgent = {
1124
+ name: "update_agent",
1125
+ label: "Update Agent",
1126
+ description: "Update an agent's configuration (status, name, prompt).",
1127
+ parameters: Type.Object({
1128
+ agent_id: Type.String(),
1129
+ status: Type.Optional(Type.String({ description: "'active', 'paused', or 'disabled'" })),
1130
+ system_prompt: Type.Optional(Type.String()),
1131
+ name: Type.Optional(Type.String()),
1132
+ }),
1133
+ async execute(toolCallId: string, params: any, signal: any) {
1134
+ const { agent_id, ...body } = params;
1135
+ const data = await apiCall("PATCH", `/v1/agents/${agent_id}`, body);
1136
+ return { content: [{ type: "text" as const, text: JSON.stringify(data) }], details: data };
1137
+ },
1138
+ renderCall(args: any, theme: any) {
1139
+ const changes = [args.status, args.name].filter(Boolean).join(", ") || "config update";
1140
+ return renderCallText("update_agent", [changes], theme);
1141
+ },
1142
+ renderResult(result: any, opts: any, theme: any) {
1143
+ const err = renderResultErr(result, theme);
1144
+ if (err) return err;
1145
+ return renderResultOk("Agent updated", [], theme);
1146
+ },
1147
+ };
1148
+
1149
+ const addAgentSkill = {
1150
+ name: "add_agent_skill",
1151
+ label: "Add Agent Skill",
1152
+ description: "Add a skill to an agent.",
1153
+ parameters: Type.Object({
1154
+ agent_id: Type.String(),
1155
+ skill_name: Type.String(),
1156
+ description: Type.String(),
1157
+ instructions: Type.Optional(Type.String()),
1158
+ }),
1159
+ async execute(toolCallId: string, params: any, signal: any) {
1160
+ const { agent_id, ...body } = params;
1161
+ const data = await apiCall("POST", `/v1/agents/${agent_id}/skills`, body);
1162
+ return { content: [{ type: "text" as const, text: JSON.stringify(data) }], details: data };
1163
+ },
1164
+ renderCall(args: any, theme: any) {
1165
+ return renderCallText("add_agent_skill", [`"${args.skill_name}"`], theme);
1166
+ },
1167
+ renderResult(result: any, opts: any, theme: any) {
1168
+ const err = renderResultErr(result, theme);
1169
+ if (err) return err;
1170
+ return renderResultOk(`Skill added: ${parseResult(result).skill_name ?? "?"}`, [], theme);
1171
+ },
1172
+ };
1173
+
1174
+ // ── All Tools ───────────────────────────────────────────────────
1175
+
1176
+ const allTools = [
1177
+ // Read tools
1178
+ getWorkspaceStatus,
1179
+ listEntities,
1180
+ getCapTable,
1181
+ listDocuments,
1182
+ listSafeNotes,
1183
+ listAgents,
1184
+ getChecklist,
1185
+ getDocumentLink,
1186
+ getSigningLink,
1187
+ listObligations,
1188
+ getBillingStatus,
1189
+ // Entity lifecycle
1190
+ formEntity,
1191
+ convertEntity,
1192
+ dissolveEntity,
1193
+ // Equity
1194
+ issueEquity,
1195
+ transferShares,
1196
+ issueSafe,
1197
+ calculateDistribution,
1198
+ // Finance
1199
+ createInvoice,
1200
+ runPayroll,
1201
+ submitPayment,
1202
+ openBankAccount,
1203
+ reconcileLedger,
1204
+ // Documents & compliance
1205
+ generateContract,
1206
+ fileTaxDocument,
1207
+ trackDeadline,
1208
+ classifyContractor,
1209
+ // Governance
1210
+ conveneMeeting,
1211
+ castVote,
1212
+ scheduleMeeting,
1213
+ // Workspace
1214
+ updateChecklist,
1215
+ // Agents
1216
+ createAgent,
1217
+ sendAgentMessage,
1218
+ updateAgent,
1219
+ addAgentSkill,
1220
+ ];
1221
+
1222
+ // ── Extension Entry Point ───────────────────────────────────────
1223
+
1224
+ export default function (pi: ExtensionAPI) {
1225
+ // Register all 30+ tools
1226
+ for (const tool of allTools) {
1227
+ pi.registerTool(tool as any);
1228
+ }
1229
+
1230
+ // ── Block built-in file write & bash tools in hosted web terminal ─
1231
+ // When running in the hosted web terminal (CORP_TERMINAL=web), Pi's
1232
+ // built-in bash/write/edit are inappropriate. In other contexts
1233
+ // (services/agents, local dev) they remain available.
1234
+ const BLOCKED_BUILTIN_TOOLS = new Set(["bash", "write", "edit"]);
1235
+ const isHostedWeb = process.env.CORP_TERMINAL === "web";
1236
+
1237
+ // ── Write Tool Confirmation ─────────────────────────────────
1238
+ pi.on("tool_call", async (event: any, ctx: any) => {
1239
+ // Headless mode: auto-approve all tool calls
1240
+ if (process.env.CORP_AUTO_APPROVE === "1") return;
1241
+
1242
+ const name = event.toolName ?? event.toolCall?.name;
1243
+ if (!name) return;
1244
+
1245
+ // Block Pi built-in file write and bash tools in hosted web terminal
1246
+ if (isHostedWeb && BLOCKED_BUILTIN_TOOLS.has(name)) {
1247
+ return {
1248
+ block: true,
1249
+ reason: `The ${name} tool is not available. Use corporate operations tools instead.`,
1250
+ };
1251
+ }
1252
+
1253
+ if (READ_ONLY_TOOLS.has(name)) return; // auto-approve reads
1254
+
1255
+ // Only intercept our corp tools
1256
+ const corpToolNames = new Set(allTools.map((t) => t.name));
1257
+ if (!corpToolNames.has(name)) return;
1258
+
1259
+ const args = event.input ?? event.toolCall?.args ?? {};
1260
+ const desc = describeToolCall(name, args);
1261
+ const approved = await ctx.ui.confirm("Confirm action", desc);
1262
+ if (!approved) return { block: true, reason: "User declined" };
1263
+ });
1264
+
1265
+ // ── System Prompt Injection ─────────────────────────────────
1266
+ pi.on("before_agent_start", async (event: any, ctx: any) => {
1267
+ const cfg = loadConfig();
1268
+ const contextLines: string[] = [];
1269
+ if (cfg.workspace_id) contextLines.push(`Workspace ID: ${cfg.workspace_id}`);
1270
+ if (cfg.active_entity_id) contextLines.push(`Active entity ID: ${cfg.active_entity_id}`);
1271
+ if (cfg.user?.name) contextLines.push(`User: ${cfg.user.name}`);
1272
+
1273
+ const corpPrompt = [
1274
+ "\n## TheCorporation Context",
1275
+ "You have corporate operations tools registered. Use them to help with entity formation, equity, finance, compliance, governance, and agent management.",
1276
+ contextLines.length > 0 ? contextLines.join("\n") : "",
1277
+ "",
1278
+ "Tool categories:",
1279
+ "- Read (auto-approved): get_workspace_status, list_entities, get_cap_table, list_documents, list_safe_notes, list_agents, get_checklist, list_obligations, get_billing_status",
1280
+ "- Entity lifecycle: form_entity, convert_entity, dissolve_entity",
1281
+ "- Equity: issue_equity, transfer_shares, issue_safe, calculate_distribution",
1282
+ "- Finance: create_invoice, run_payroll, submit_payment, open_bank_account, reconcile_ledger",
1283
+ "- Documents: generate_contract, file_tax_document, track_deadline, classify_contractor, get_signing_link, get_document_link",
1284
+ "- Governance: convene_meeting, cast_vote, schedule_meeting",
1285
+ "- Agents: create_agent, send_agent_message, update_agent, add_agent_skill",
1286
+ "",
1287
+ isHostedWeb ? "IMPORTANT: Do NOT use the bash, write, or edit tools. They are disabled. You are a corporate operations agent — use only the corporate tools listed above." : "",
1288
+ "Rules: All monetary values in cents. LLC→US-WY, Corporation→US-DE. Documents can ONLY be signed through signing links. Always suggest next steps after actions.",
1289
+ ].filter(Boolean).join("\n");
1290
+
1291
+ return {
1292
+ systemPrompt: (event.systemPrompt ?? "") + corpPrompt,
1293
+ };
1294
+ });
1295
+
1296
+ // ── Boot Screen ─────────────────────────────────────────────
1297
+ pi.on("session_start", async (_event: any, ctx: any) => {
1298
+ if (!ctx.hasUI || process.env.CORP_AUTO_APPROVE === "1") return;
1299
+ const cfg = loadConfig();
1300
+ const ws = cfg.workspace_id ? cfg.workspace_id.slice(0, 8) + "…" : "not configured";
1301
+ const api = cfg.api_url || "not configured";
1302
+ const user = cfg.user?.name || "anonymous";
1303
+ const entity = cfg.active_entity_id ? cfg.active_entity_id.slice(0, 8) + "…" : "none";
1304
+
1305
+ const lines = [
1306
+ "",
1307
+ " ╔══════════════════════════════════════════╗",
1308
+ " ║ ║",
1309
+ " ║ T H E C O R P O R A T I O N ║",
1310
+ " ║ Corporate Operations Agent ║",
1311
+ " ║ ║",
1312
+ " ╚══════════════════════════════════════════╝",
1313
+ "",
1314
+ ` Workspace: ${ws}`,
1315
+ ` API: ${api}`,
1316
+ ` User: ${user}`,
1317
+ ` Entity: ${entity}`,
1318
+ "",
1319
+ " Commands: /status /entities /cap-table",
1320
+ " /obligations /documents /billing",
1321
+ "",
1322
+ " 35 corporate tools loaded. Write tools require approval.",
1323
+ "",
1324
+ ];
1325
+ ctx.ui.notify(lines.join("\n"), "info");
1326
+ });
1327
+
1328
+ // ── Headless Result Output ──────────────────────────────────
1329
+ // When running in headless mode (CORP_AUTO_APPROVE=1), write an
1330
+ // ExecutionResult-compatible JSON file so the orchestrator can collect it.
1331
+ pi.on("session_end", async (event: any, _ctx: any) => {
1332
+ if (process.env.CORP_AUTO_APPROVE !== "1") return;
1333
+ const resultPath = join(process.env.WORKSPACE || "/workspace", ".result.json");
1334
+ const result = {
1335
+ success: !event.error,
1336
+ reason: event.error?.message ?? null,
1337
+ final_response: event.lastAssistantMessage ?? "",
1338
+ tool_calls_count: event.stats?.toolCallCount ?? 0,
1339
+ turns: event.stats?.turnCount ?? 0,
1340
+ input_tokens: event.stats?.inputTokens ?? 0,
1341
+ output_tokens: event.stats?.outputTokens ?? 0,
1342
+ transcript: [],
1343
+ tasks: [],
1344
+ };
1345
+ writeFileSync(resultPath, JSON.stringify(result));
1346
+ });
1347
+
1348
+ // ── LLM Provider Config (Headless) ────────────────────────
1349
+ // When running headless with a secrets proxy, configure Pi to route
1350
+ // LLM calls through the proxy URL with the opaque API key token.
1351
+ if (process.env.CORP_AUTO_APPROVE === "1" && process.env.CORP_LLM_PROXY_URL) {
1352
+ const piDir = join(process.env.PI_CODING_AGENT_DIR || join(homedir(), ".pi", "agent"));
1353
+ mkdirSync(piDir, { recursive: true });
1354
+ writeFileSync(join(piDir, "models.json"), JSON.stringify({
1355
+ providers: {
1356
+ openrouter: {
1357
+ baseUrl: process.env.CORP_LLM_PROXY_URL,
1358
+ apiKey: process.env.OPENROUTER_API_KEY || "",
1359
+ },
1360
+ },
1361
+ }));
1362
+ }
1363
+
1364
+ // ── Slash Commands ──────────────────────────────────────────
1365
+
1366
+ pi.registerCommand("status", {
1367
+ description: "Show workspace status summary",
1368
+ handler: async (args: string, ctx: any) => {
1369
+ try {
1370
+ const data = await apiCall("GET", `/v1/workspaces/${wsId()}/status`);
1371
+ const lines = [
1372
+ `Workspace: ${data.workspace_name ?? data.name ?? wsId()}`,
1373
+ `Entities: ${data.entity_count ?? data.entities ?? "?"}`,
1374
+ `Documents: ${data.document_count ?? data.documents ?? "?"}`,
1375
+ `Grants: ${data.grant_count ?? data.grants ?? "?"}`,
1376
+ `Tier: ${data.billing_tier ?? data.tier ?? "free"}`,
1377
+ ];
1378
+ ctx.ui.notify(lines.join("\n"), "info");
1379
+ } catch (e: any) {
1380
+ ctx.ui.notify(`Failed to fetch status: ${e.message}`, "error");
1381
+ }
1382
+ },
1383
+ });
1384
+
1385
+ pi.registerCommand("entities", {
1386
+ description: "List entities in the workspace",
1387
+ handler: async (args: string, ctx: any) => {
1388
+ try {
1389
+ const data = await apiCall("GET", `/v1/workspaces/${wsId()}/entities`);
1390
+ const entities = Array.isArray(data) ? data : data.entities ?? [];
1391
+ if (entities.length === 0) {
1392
+ ctx.ui.notify("No entities found. Use form_entity to create one.", "info");
1393
+ return;
1394
+ }
1395
+ const items = entities.map((e: any) =>
1396
+ `${e.legal_name ?? e.entity_name ?? e.name} | ${e.entity_type} | ${e.jurisdiction ?? "?"} | ${e.status ?? "active"}`
1397
+ );
1398
+ const choice = await ctx.ui.select("Entities:", items);
1399
+ if (choice != null && entities[choice]) {
1400
+ const eid = entities[choice].entity_id ?? entities[choice].id;
1401
+ ctx.ui.notify(`Selected entity: ${eid}`, "info");
1402
+ }
1403
+ } catch (e: any) {
1404
+ ctx.ui.notify(`Failed to list entities: ${e.message}`, "error");
1405
+ }
1406
+ },
1407
+ });
1408
+
1409
+ pi.registerCommand("cap-table", {
1410
+ description: "Show cap table for the active entity",
1411
+ handler: async (args: string, ctx: any) => {
1412
+ const cfg = loadConfig();
1413
+ const entityId = args.trim() || cfg.active_entity_id;
1414
+ if (!entityId) {
1415
+ ctx.ui.notify("No active entity. Pass an entity ID or set active_entity_id in ~/.corp/config.json", "warning");
1416
+ return;
1417
+ }
1418
+ try {
1419
+ const data = await apiCall("GET", `/v1/entities/${entityId}/cap-table`);
1420
+ const grants = data.grants ?? data.rows ?? [];
1421
+ if (grants.length === 0) {
1422
+ ctx.ui.notify("Cap table is empty.", "info");
1423
+ return;
1424
+ }
1425
+ const lines = grants.map((g: any) => {
1426
+ const pct = g.percentage != null ? (g.percentage * 100).toFixed(1) + "%" : "?";
1427
+ return `${g.holder_name ?? g.recipient_name ?? "?"} | ${g.shares ?? "?"} ${g.grant_type ?? ""} | ${pct}`;
1428
+ });
1429
+ ctx.ui.notify("Cap Table:\n" + lines.join("\n"), "info");
1430
+ } catch (e: any) {
1431
+ ctx.ui.notify(`Failed to fetch cap table: ${e.message}`, "error");
1432
+ }
1433
+ },
1434
+ });
1435
+
1436
+ pi.registerCommand("obligations", {
1437
+ description: "Show obligations with urgency coloring",
1438
+ handler: async (args: string, ctx: any) => {
1439
+ try {
1440
+ const data = await apiCall("GET", "/v1/obligations/summary");
1441
+ const items = data.obligations ?? data.items ?? [];
1442
+ if (items.length === 0) {
1443
+ ctx.ui.notify("No obligations found.", "info");
1444
+ return;
1445
+ }
1446
+ const lines = items.map((o: any) => {
1447
+ const prefix = o.tier === "overdue" ? "🔴" : o.tier === "due_today" ? "🟡" : "🟢";
1448
+ return `${prefix} [${o.tier ?? "?"}] ${o.description ?? o.title ?? "obligation"} — due ${o.due_date ?? "?"}`;
1449
+ });
1450
+ ctx.ui.notify("Obligations:\n" + lines.join("\n"), "info");
1451
+ } catch (e: any) {
1452
+ ctx.ui.notify(`Failed to fetch obligations: ${e.message}`, "error");
1453
+ }
1454
+ },
1455
+ });
1456
+
1457
+ pi.registerCommand("documents", {
1458
+ description: "List documents for the active entity",
1459
+ handler: async (args: string, ctx: any) => {
1460
+ const cfg = loadConfig();
1461
+ const entityId = args.trim() || cfg.active_entity_id;
1462
+ if (!entityId) {
1463
+ ctx.ui.notify("No active entity. Pass an entity ID or set active_entity_id.", "warning");
1464
+ return;
1465
+ }
1466
+ try {
1467
+ const data = await apiCall("GET", `/v1/formations/${entityId}/documents`);
1468
+ const docs = Array.isArray(data) ? data : data.documents ?? [];
1469
+ if (docs.length === 0) {
1470
+ ctx.ui.notify("No documents found.", "info");
1471
+ return;
1472
+ }
1473
+ const badge = (s: string) =>
1474
+ s === "signed" ? "✓" : s === "pending_signature" || s === "pending" ? "⏳" : "📝";
1475
+ const lines = docs.map((d: any) =>
1476
+ `${badge(d.status)} ${d.title ?? d.document_type ?? "doc"} [${d.status ?? "draft"}]`
1477
+ );
1478
+ ctx.ui.notify("Documents:\n" + lines.join("\n"), "info");
1479
+ } catch (e: any) {
1480
+ ctx.ui.notify(`Failed to fetch documents: ${e.message}`, "error");
1481
+ }
1482
+ },
1483
+ });
1484
+
1485
+ pi.registerCommand("billing", {
1486
+ description: "Show billing status and plan info",
1487
+ handler: async (args: string, ctx: any) => {
1488
+ try {
1489
+ const [status, plansData] = await Promise.all([
1490
+ apiCall("GET", `/v1/billing/status?workspace_id=${wsId()}`),
1491
+ apiCall("GET", "/v1/billing/plans"),
1492
+ ]);
1493
+ const plans = plansData?.plans ?? plansData ?? [];
1494
+ const lines = [
1495
+ `Current tier: ${status.tier ?? status.plan ?? "free"}`,
1496
+ `Tool calls: ${status.tool_call_count ?? "?"}/${status.tool_call_limit ?? "?"}`,
1497
+ ];
1498
+ if (Array.isArray(plans) && plans.length > 0) {
1499
+ lines.push("", "Available plans:");
1500
+ for (const p of plans) {
1501
+ lines.push(` ${p.name ?? p.tier}: ${p.price ?? "?"}`);
1502
+ }
1503
+ }
1504
+ ctx.ui.notify(lines.join("\n"), "info");
1505
+ } catch (e: any) {
1506
+ ctx.ui.notify(`Failed to fetch billing: ${e.message}`, "error");
1507
+ }
1508
+ },
1509
+ });
1510
+ }