indusagi-coding-agent 0.2.8 → 0.2.9
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/package.json +2 -1
- package/src/_decls/entry.ts +18 -0
- package/src/_decls/guardrails.ts +35 -0
- package/src/_decls/index.ts +26 -0
- package/src/addons/contract.ts +236 -0
- package/src/addons/dispatch/event-dispatcher.ts +164 -0
- package/src/addons/dispatch/index.ts +25 -0
- package/src/addons/dispatch/tool-interceptor.ts +208 -0
- package/src/addons/host.ts +225 -0
- package/src/addons/index.ts +112 -0
- package/src/addons/manifest.ts +158 -0
- package/src/addons/sandbox.ts +170 -0
- package/src/addons/surface.ts +78 -0
- package/src/boot/auth-vault.ts +195 -0
- package/src/boot/boot.ts +138 -0
- package/src/boot/contract.ts +238 -0
- package/src/boot/heap.ts +59 -0
- package/src/boot/index.ts +28 -0
- package/src/boot/invocation.ts +93 -0
- package/src/boot/runners/addon-wiring.ts +153 -0
- package/src/boot/runners/checkpoint.ts +169 -0
- package/src/boot/runners/delegate-runner.ts +294 -0
- package/src/boot/runners/index.ts +13 -0
- package/src/boot/runners/link-runner.ts +45 -0
- package/src/boot/runners/memdir.ts +168 -0
- package/src/boot/runners/oneshot-runner.ts +58 -0
- package/src/boot/runners/read-state.ts +90 -0
- package/src/boot/runners/registry.ts +42 -0
- package/src/boot/runners/repl-runner.ts +143 -0
- package/src/boot/runners/server-mode.ts +121 -0
- package/src/boot/runners/session.ts +641 -0
- package/src/boot/server-token.ts +148 -0
- package/src/boot/stages.ts +167 -0
- package/src/boot/upgrade/apply.ts +94 -0
- package/src/boot/upgrade/index.ts +13 -0
- package/src/boot/upgrade/upgrades.ts +289 -0
- package/src/briefing/compose.ts +150 -0
- package/src/briefing/context-docs.ts +19 -0
- package/src/briefing/contract.ts +717 -0
- package/src/briefing/index.ts +31 -0
- package/src/briefing/macros.ts +97 -0
- package/src/briefing/skills.ts +47 -0
- package/src/capability-deck/bridge-ledger/index.ts +27 -0
- package/src/capability-deck/bridge-ledger/key.ts +67 -0
- package/src/capability-deck/bridge-ledger/ledger.ts +131 -0
- package/src/capability-deck/bridge-ledger/network.ts +117 -0
- package/src/capability-deck/builtin-bridge.ts +312 -0
- package/src/capability-deck/cards/bg-process-card.ts +335 -0
- package/src/capability-deck/cards/index.ts +115 -0
- package/src/capability-deck/cards/memory-card.ts +146 -0
- package/src/capability-deck/cards/plan-file.ts +97 -0
- package/src/capability-deck/cards/plan-tools.ts +185 -0
- package/src/capability-deck/cards/saas-card.ts +183 -0
- package/src/capability-deck/cards/task-card.ts +207 -0
- package/src/capability-deck/cards/todo-card.ts +168 -0
- package/src/capability-deck/cards/workflow-card.ts +247 -0
- package/src/capability-deck/contract.ts +388 -0
- package/src/capability-deck/index.ts +48 -0
- package/src/capability-deck/manifest.ts +109 -0
- package/src/capability-deck/provision.ts +169 -0
- package/src/channels/contract.ts +191 -0
- package/src/channels/framer.ts +50 -0
- package/src/channels/index.ts +101 -0
- package/src/channels/link/dialog.ts +129 -0
- package/src/channels/link/driver.ts +190 -0
- package/src/channels/link/index.ts +34 -0
- package/src/channels/link/server.ts +134 -0
- package/src/channels/oneshot.ts +90 -0
- package/src/channels/ops.ts +65 -0
- package/src/channels/session-ops.ts +81 -0
- package/src/conductor/bash-guard.ts +599 -0
- package/src/conductor/catalog/catalog.ts +116 -0
- package/src/conductor/catalog/index.ts +8 -0
- package/src/conductor/catalog/matcher.ts +134 -0
- package/src/conductor/conductor.ts +234 -0
- package/src/conductor/contract.ts +842 -0
- package/src/conductor/diagnostics.ts +227 -0
- package/src/conductor/index.ts +33 -0
- package/src/conductor/permissions.ts +588 -0
- package/src/conductor/quota-error.ts +49 -0
- package/src/conductor/signal-hub/hub.ts +46 -0
- package/src/conductor/signal-hub/index.ts +2 -0
- package/src/conductor/signal-hub/translate.test.ts +81 -0
- package/src/conductor/signal-hub/translate.ts +74 -0
- package/src/conductor/skill-parse/index.ts +2 -0
- package/src/conductor/skill-parse/parse.ts +108 -0
- package/src/conductor/transcript-store/index.ts +22 -0
- package/src/conductor/transcript-store/serialize.ts +116 -0
- package/src/conductor/transcript-store/store.ts +205 -0
- package/src/console/auth-status.ts +56 -0
- package/src/console/components/AgentsView.ts +165 -0
- package/src/console/components/BackgroundAgents.ts +155 -0
- package/src/console/components/Banner.ts +334 -0
- package/src/console/components/Composer.ts +94 -0
- package/src/console/components/StatusBar.ts +49 -0
- package/src/console/components/TerminalConsole.ts +1090 -0
- package/src/console/components/WorkingIndicator.ts +98 -0
- package/src/console/components/banner-sweep.ts +24 -0
- package/src/console/components/welcome.ts +74 -0
- package/src/console/contract.ts +630 -0
- package/src/console/index.ts +34 -0
- package/src/console/input/complete.ts +127 -0
- package/src/console/input/dir-reader.ts +34 -0
- package/src/console/input/index.ts +23 -0
- package/src/console/input/keymap.ts +159 -0
- package/src/console/input/paste.ts +104 -0
- package/src/console/mount.ts +56 -0
- package/src/console/overlays/approval-queue.ts +57 -0
- package/src/console/overlays/approval.ts +130 -0
- package/src/console/overlays/auth.ts +342 -0
- package/src/console/overlays/boards.ts +308 -0
- package/src/console/overlays/host.ts +36 -0
- package/src/console/overlays/index.ts +26 -0
- package/src/console/overlays/pickers.ts +258 -0
- package/src/console/overlays/sessions.ts +190 -0
- package/src/console/reducer.ts +182 -0
- package/src/console/slash/builtins.ts +81 -0
- package/src/console/slash/commands/dynamic.ts +83 -0
- package/src/console/slash/commands/integrations.ts +695 -0
- package/src/console/slash/commands/shared.ts +75 -0
- package/src/console/slash/commands/transcript.ts +263 -0
- package/src/console/slash/commands/workbench.ts +246 -0
- package/src/console/slash/index.ts +15 -0
- package/src/console/slash/registry.ts +70 -0
- package/src/console/slash/resolve.ts +63 -0
- package/src/console/startup.ts +209 -0
- package/src/console/theme/adapter.ts +45 -0
- package/src/console/theme/index.ts +7 -0
- package/src/console/theme/palette.ts +68 -0
- package/src/console/theme/resolve.ts +39 -0
- package/src/console/theme/tokens.ts +71 -0
- package/src/entry.ts +55 -0
- package/src/guardrails.ts +37 -0
- package/src/index.ts +18 -0
- package/src/insight/channel.ts +88 -0
- package/src/insight/contract.ts +185 -0
- package/src/insight/index.ts +110 -0
- package/src/insight/recorder.ts +213 -0
- package/src/insight/redaction.ts +157 -0
- package/src/insight/replay.ts +158 -0
- package/src/insight/sampling.ts +70 -0
- package/src/insight/serialize.ts +50 -0
- package/src/insight/sinks/console.ts +64 -0
- package/src/insight/sinks/file.ts +40 -0
- package/src/insight/sinks/index.ts +24 -0
- package/src/insight/sinks/stream.ts +54 -0
- package/src/integrations/sarvam/attach.ts +239 -0
- package/src/integrations/sarvam/config.ts +156 -0
- package/src/integrations/sarvam/index.ts +25 -0
- package/src/integrations/sarvam/sarvam.test.ts +60 -0
- package/src/integrations/sarvam/types.ts +27 -0
- package/src/integrations/zoho/attach.ts +342 -0
- package/src/integrations/zoho/config.ts +125 -0
- package/src/integrations/zoho/index.ts +27 -0
- package/src/integrations/zoho/types.ts +21 -0
- package/src/integrations/zoho/zoho.test.ts +50 -0
- package/src/kit/clipboard-image.ts +107 -0
- package/src/kit/external-editor.ts +48 -0
- package/src/kit/image.ts +59 -0
- package/src/kit/index.ts +51 -0
- package/src/kit/shell.ts +19 -0
- package/src/kit/tool-fetch.ts +85 -0
- package/src/launch/catalog.ts +148 -0
- package/src/launch/contract.ts +187 -0
- package/src/launch/credentials.ts +625 -0
- package/src/launch/index.ts +98 -0
- package/src/launch/invocation/attachments.ts +179 -0
- package/src/launch/invocation/flags.ts +196 -0
- package/src/launch/invocation/index.ts +25 -0
- package/src/launch/invocation/read.ts +260 -0
- package/src/launch/invocation/usage.ts +67 -0
- package/src/launch/login.ts +324 -0
- package/src/launch/oauth.test.ts +18 -0
- package/src/launch/oauth.ts +203 -0
- package/src/launch/packages.ts +194 -0
- package/src/launch/pickers.ts +189 -0
- package/src/runtime-bridge/bridges/_drive.ts +96 -0
- package/src/runtime-bridge/bridges/builtins.ts +68 -0
- package/src/runtime-bridge/bridges/claude-cli.ts +123 -0
- package/src/runtime-bridge/bridges/codex-cli.ts +142 -0
- package/src/runtime-bridge/bridges/index.ts +33 -0
- package/src/runtime-bridge/bridges/indusagi-cli.ts +155 -0
- package/src/runtime-bridge/broker.ts +227 -0
- package/src/runtime-bridge/contract.ts +122 -0
- package/src/runtime-bridge/index.ts +79 -0
- package/src/runtime-bridge/sink.ts +180 -0
- package/src/sessions/contract.ts +81 -0
- package/src/sessions/index.ts +13 -0
- package/src/sessions/library.ts +229 -0
- package/src/settings/contract.ts +114 -0
- package/src/settings/index.ts +32 -0
- package/src/settings/manager.ts +117 -0
- package/src/transcript-export/index.ts +45 -0
- package/src/transcript-export/publish.ts +260 -0
- package/src/transcript-export/sgr.ts +315 -0
- package/src/transcript-export/template.ts +272 -0
- package/src/transcript-export/theme-bridge.ts +150 -0
- package/src/window-budget/budget/estimate.ts +135 -0
- package/src/window-budget/budget/gate.ts +33 -0
- package/src/window-budget/budget/index.ts +16 -0
- package/src/window-budget/budget/slice.ts +56 -0
- package/src/window-budget/condenser.ts +58 -0
- package/src/window-budget/contract.ts +184 -0
- package/src/window-budget/index.ts +19 -0
- package/src/window-budget/microcompact.ts +95 -0
- package/src/window-budget/rehydrate.ts +136 -0
- package/src/window-budget/summarize/condense.ts +103 -0
- package/src/window-budget/summarize/index.ts +14 -0
- package/src/window-budget/summarize/prompt.ts +149 -0
- package/src/workflow-engine/agent-runner.ts +181 -0
- package/src/workflow-engine/display.ts +224 -0
- package/src/workflow-engine/engine.ts +294 -0
- package/src/workflow-engine/index.ts +23 -0
- package/src/workflow-engine/parse.ts +172 -0
- package/src/workflow-engine/structured-output.ts +35 -0
- package/src/workspace/brand.ts +29 -0
- package/src/workspace/index.ts +18 -0
- package/src/workspace/locator.ts +103 -0
- package/src/workspace/runtime-detect.ts +64 -0
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Attach Zoho MCP tools to the coding agent via indusagi/zoho.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
6
|
+
import { homedir } from "node:os";
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
import { connectZohoMcp } from "indusagi/zoho";
|
|
9
|
+
import { createMCPAgentToolFactory } from "indusagi/mcp";
|
|
10
|
+
import type { ZohoAttachResult, ZohoHostConfig } from "./types.js";
|
|
11
|
+
import { resolveZohoConfig, type ResolveZohoOptions } from "./config.js";
|
|
12
|
+
|
|
13
|
+
/** Live connection handle for /zoho status and disconnect. */
|
|
14
|
+
let live: {
|
|
15
|
+
id: string;
|
|
16
|
+
toolNames: readonly string[];
|
|
17
|
+
close: () => Promise<void>;
|
|
18
|
+
urlRedacted: string;
|
|
19
|
+
} | null = null;
|
|
20
|
+
|
|
21
|
+
/** In-flight connect (so disconnect / UI can track). */
|
|
22
|
+
let connecting = false;
|
|
23
|
+
/** If true, tear down as soon as connect resolves. */
|
|
24
|
+
let cancelConnect = false;
|
|
25
|
+
/** Last OAuth authorize URL shown to the user (for overlay / status). */
|
|
26
|
+
let lastAuthUrl: string | undefined;
|
|
27
|
+
|
|
28
|
+
export function getLiveZoho(): typeof live {
|
|
29
|
+
return live;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function isZohoConnecting(): boolean {
|
|
33
|
+
return connecting;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function getLastZohoAuthUrl(): string | undefined {
|
|
37
|
+
return lastAuthUrl;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Default credentials path used by the framework OAuth provider. */
|
|
41
|
+
function zohoTokenPath(id = "zoho"): string {
|
|
42
|
+
const xdg = process.env.XDG_CONFIG_HOME;
|
|
43
|
+
const home = process.env.HOME ?? process.env.USERPROFILE ?? homedir();
|
|
44
|
+
const dir =
|
|
45
|
+
xdg && xdg.length > 0
|
|
46
|
+
? join(xdg, "indusagi", "credentials", "mcp")
|
|
47
|
+
: join(home, ".config", "indusagi", "credentials", "mcp");
|
|
48
|
+
return join(dir, `${id.replace(/[^a-zA-Z0-9._-]/g, "-")}.json`);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* True when a usable access_token is already on disk (skip browser at boot).
|
|
53
|
+
*/
|
|
54
|
+
export function hasUsableZohoTokens(id = "zoho"): boolean {
|
|
55
|
+
const path = zohoTokenPath(id);
|
|
56
|
+
try {
|
|
57
|
+
if (!existsSync(path)) return false;
|
|
58
|
+
const raw = readFileSync(path, "utf8");
|
|
59
|
+
const parsed = JSON.parse(raw) as { tokens?: { access_token?: string } };
|
|
60
|
+
return typeof parsed?.tokens?.access_token === "string" && parsed.tokens.access_token.length > 0;
|
|
61
|
+
} catch {
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function extractAuthUrl(message: string): string | undefined {
|
|
67
|
+
// Framework logs: "oauth: open this URL…\n https://…"
|
|
68
|
+
const multi = message.match(/https?:\/\/[^\s]+/g);
|
|
69
|
+
if (!multi || multi.length === 0) return undefined;
|
|
70
|
+
// Prefer the authorize URL (not the local callback).
|
|
71
|
+
const auth = multi.find((u) => !u.includes("127.0.0.1") && !u.includes("localhost"));
|
|
72
|
+
return auth ?? multi[0];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Convert a connected Zoho MCP client into AgentTool[] using the same factory
|
|
77
|
+
* the coding agent uses for generic MCP servers (`createMCPAgentToolFactory`).
|
|
78
|
+
* Tool names become `{serverName}_{mcpToolName}` (e.g. zoho_getLeadsRecords).
|
|
79
|
+
*/
|
|
80
|
+
async function toolsFromConnection(
|
|
81
|
+
conn: Awaited<ReturnType<typeof connectZohoMcp>>,
|
|
82
|
+
): Promise<unknown[]> {
|
|
83
|
+
const tools: unknown[] = [];
|
|
84
|
+
const client = conn.client as {
|
|
85
|
+
listTools: () => Promise<
|
|
86
|
+
Array<{ name: string; description?: string; inputSchema: Record<string, unknown> }>
|
|
87
|
+
>;
|
|
88
|
+
serverName: string;
|
|
89
|
+
connected: boolean;
|
|
90
|
+
callTool: (name: string, args: Record<string, unknown>) => Promise<unknown>;
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
try {
|
|
94
|
+
const defs = await client.listTools();
|
|
95
|
+
for (const def of defs) {
|
|
96
|
+
const factory = createMCPAgentToolFactory(def, client as never);
|
|
97
|
+
tools.push(factory());
|
|
98
|
+
}
|
|
99
|
+
} catch (err) {
|
|
100
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
101
|
+
console.error(`[zoho] listTools/factory failed: ${message}`);
|
|
102
|
+
}
|
|
103
|
+
return tools;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export interface AttachZohoOptions {
|
|
107
|
+
closables?: Array<() => Promise<void>>;
|
|
108
|
+
/** Always try to open the system browser (default true for interactive). */
|
|
109
|
+
openBrowser?: boolean;
|
|
110
|
+
/** Called when framework logs OAuth progress (includes authorize URL). */
|
|
111
|
+
onAuthMessage?: (message: string) => void;
|
|
112
|
+
/** Called when an authorize URL is detected. */
|
|
113
|
+
onAuthUrl?: (url: string) => void;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Connect to Zoho MCP and return AgentTool-compatible tools.
|
|
118
|
+
* Registers close on opts.closables when provided.
|
|
119
|
+
*/
|
|
120
|
+
export async function attachZohoTools(
|
|
121
|
+
config: ZohoHostConfig,
|
|
122
|
+
opts?: AttachZohoOptions,
|
|
123
|
+
): Promise<ZohoAttachResult> {
|
|
124
|
+
// Tear down previous live connection if any
|
|
125
|
+
if (live) {
|
|
126
|
+
try {
|
|
127
|
+
await live.close();
|
|
128
|
+
} catch {
|
|
129
|
+
/* ignore */
|
|
130
|
+
}
|
|
131
|
+
live = null;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
connecting = true;
|
|
135
|
+
cancelConnect = false;
|
|
136
|
+
lastAuthUrl = undefined;
|
|
137
|
+
|
|
138
|
+
const onAuthMessage = opts?.onAuthMessage;
|
|
139
|
+
const onAuthUrl = opts?.onAuthUrl;
|
|
140
|
+
const openBrowser = opts?.openBrowser !== false;
|
|
141
|
+
|
|
142
|
+
const handleLog = (msg: string) => {
|
|
143
|
+
try {
|
|
144
|
+
onAuthMessage?.(msg);
|
|
145
|
+
} catch {
|
|
146
|
+
/* ignore UI errors */
|
|
147
|
+
}
|
|
148
|
+
const url = extractAuthUrl(msg);
|
|
149
|
+
if (url) {
|
|
150
|
+
lastAuthUrl = url;
|
|
151
|
+
try {
|
|
152
|
+
onAuthUrl?.(url);
|
|
153
|
+
} catch {
|
|
154
|
+
/* ignore */
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
try {
|
|
160
|
+
const conn = await connectZohoMcp({
|
|
161
|
+
url: config.url,
|
|
162
|
+
id: config.id ?? "zoho",
|
|
163
|
+
auth: config.auth ?? "auto",
|
|
164
|
+
timeoutMs: 60_000,
|
|
165
|
+
// Force browser open for interactive connect (don't gate only on isTTY).
|
|
166
|
+
openBrowser,
|
|
167
|
+
logger: {
|
|
168
|
+
debug: (msg) => handleLog(msg),
|
|
169
|
+
error: (msg, err) => {
|
|
170
|
+
const detail = err instanceof Error ? err.message : err !== undefined ? String(err) : "";
|
|
171
|
+
handleLog(detail ? `${msg}: ${detail}` : msg);
|
|
172
|
+
console.error(`[zoho error] ${msg}${detail ? `: ${detail}` : ""}`);
|
|
173
|
+
},
|
|
174
|
+
},
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
if (cancelConnect) {
|
|
178
|
+
try {
|
|
179
|
+
await conn.close();
|
|
180
|
+
} catch {
|
|
181
|
+
/* ignore */
|
|
182
|
+
}
|
|
183
|
+
throw new Error("Zoho connect cancelled (disconnect).");
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const tools = await toolsFromConnection(conn);
|
|
187
|
+
// Prefer names the agent actually calls (createMCPAgentToolFactory),
|
|
188
|
+
// fall back to framework-prefixed names from connectZohoMcp.
|
|
189
|
+
const fromTools = tools
|
|
190
|
+
.map((t) => (t as { name?: string }).name)
|
|
191
|
+
.filter((n): n is string => typeof n === "string" && n.length > 0);
|
|
192
|
+
const toolNames =
|
|
193
|
+
fromTools.length > 0
|
|
194
|
+
? fromTools
|
|
195
|
+
: conn.toolNames.length > 0
|
|
196
|
+
? [...conn.toolNames]
|
|
197
|
+
: [];
|
|
198
|
+
|
|
199
|
+
const close = async () => {
|
|
200
|
+
try {
|
|
201
|
+
await conn.close();
|
|
202
|
+
} finally {
|
|
203
|
+
if (live?.id === (config.id ?? "zoho")) live = null;
|
|
204
|
+
}
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
opts?.closables?.push(close);
|
|
208
|
+
|
|
209
|
+
let urlRedacted = config.url;
|
|
210
|
+
try {
|
|
211
|
+
const u = new URL(config.url);
|
|
212
|
+
urlRedacted = `${u.protocol}//${u.host}/…`;
|
|
213
|
+
} catch {
|
|
214
|
+
urlRedacted = "(url)";
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
live = {
|
|
218
|
+
id: config.id ?? "zoho",
|
|
219
|
+
toolNames,
|
|
220
|
+
close,
|
|
221
|
+
urlRedacted,
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
return { tools, toolNames, id: config.id ?? "zoho", close };
|
|
225
|
+
} finally {
|
|
226
|
+
connecting = false;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Session helper: resolve config and attach, or return [] if Zoho is off.
|
|
232
|
+
*
|
|
233
|
+
* Boot-safe: if there is no usable OAuth token yet, skip auto-attach so we
|
|
234
|
+
* never hang the CLI for 5 minutes waiting for a browser. Interactive
|
|
235
|
+
* `/zoho connect` still runs the full OAuth flow.
|
|
236
|
+
*/
|
|
237
|
+
export async function loadZohoAgentTools(opts: {
|
|
238
|
+
cwd: string;
|
|
239
|
+
closables?: Array<() => Promise<void>>;
|
|
240
|
+
/** From invocation flags */
|
|
241
|
+
zohoUrl?: string;
|
|
242
|
+
noZoho?: boolean;
|
|
243
|
+
forceZoho?: boolean;
|
|
244
|
+
/** Allow browser OAuth during load (default false — use /zoho connect). */
|
|
245
|
+
allowBrowserOAuth?: boolean;
|
|
246
|
+
}): Promise<unknown[]> {
|
|
247
|
+
const resolveOpts: ResolveZohoOptions = {
|
|
248
|
+
cwd: opts.cwd,
|
|
249
|
+
url: opts.zohoUrl,
|
|
250
|
+
disabled: opts.noZoho,
|
|
251
|
+
forceEnable: opts.forceZoho,
|
|
252
|
+
};
|
|
253
|
+
const config = resolveZohoConfig(resolveOpts);
|
|
254
|
+
if (!config) return [];
|
|
255
|
+
|
|
256
|
+
const allowBrowser = opts.allowBrowserOAuth === true;
|
|
257
|
+
if (!allowBrowser && !hasUsableZohoTokens(config.id ?? "zoho")) {
|
|
258
|
+
// Soft skip: user has URL configured but has not completed OAuth yet.
|
|
259
|
+
console.error(
|
|
260
|
+
"[zoho] URL configured but no OAuth tokens yet — run /zoho-connect to authorize in the browser.",
|
|
261
|
+
);
|
|
262
|
+
return [];
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
try {
|
|
266
|
+
const result = await attachZohoTools(config, {
|
|
267
|
+
closables: opts.closables,
|
|
268
|
+
openBrowser: allowBrowser,
|
|
269
|
+
});
|
|
270
|
+
return [...result.tools];
|
|
271
|
+
} catch (err) {
|
|
272
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
273
|
+
console.error(`[zoho] failed to attach: ${message}`);
|
|
274
|
+
return [];
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
export async function disconnectLiveZoho(): Promise<boolean> {
|
|
279
|
+
// Cancel in-flight connect if any
|
|
280
|
+
if (connecting) {
|
|
281
|
+
cancelConnect = true;
|
|
282
|
+
}
|
|
283
|
+
if (!live) {
|
|
284
|
+
return connecting; // "was connecting" still counts as handled
|
|
285
|
+
}
|
|
286
|
+
try {
|
|
287
|
+
await live.close();
|
|
288
|
+
} catch {
|
|
289
|
+
/* ignore */
|
|
290
|
+
}
|
|
291
|
+
live = null;
|
|
292
|
+
return true;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
export function zohoStatusLines(): string {
|
|
296
|
+
if (connecting) {
|
|
297
|
+
const lines = [
|
|
298
|
+
"Zoho MCP: connecting… (OAuth may open your browser)",
|
|
299
|
+
"Complete login in the browser, then wait for tools to load.",
|
|
300
|
+
"Run /zoho-disconnect to cancel.",
|
|
301
|
+
];
|
|
302
|
+
if (lastAuthUrl) {
|
|
303
|
+
lines.push("", "If the browser did not open, visit this URL:", ` ${lastAuthUrl}`);
|
|
304
|
+
}
|
|
305
|
+
return lines.join("\n");
|
|
306
|
+
}
|
|
307
|
+
if (!live) {
|
|
308
|
+
return [
|
|
309
|
+
"Zoho MCP: not connected.",
|
|
310
|
+
"Set ZOHO_MCP_URL or run /zoho-connect <url>.",
|
|
311
|
+
"Tokens (after OAuth): ~/.config/indusagi/credentials/mcp/zoho.json",
|
|
312
|
+
"",
|
|
313
|
+
"Commands: /zoho-connect · /zoho-disconnect · /zoho-status · /zoho-tools · /zoho-help",
|
|
314
|
+
].join("\n");
|
|
315
|
+
}
|
|
316
|
+
return [
|
|
317
|
+
`Zoho MCP: connected (id=${live.id}).`,
|
|
318
|
+
`Endpoint: ${live.urlRedacted}`,
|
|
319
|
+
`Tools (${live.toolNames.length}):`,
|
|
320
|
+
...live.toolNames.map((n) => ` • ${n}`),
|
|
321
|
+
"",
|
|
322
|
+
"Run /zoho-disconnect to close the connection.",
|
|
323
|
+
].join("\n");
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/** Overlay text while waiting for browser OAuth. */
|
|
327
|
+
export function zohoOAuthPendingText(authUrl?: string): string {
|
|
328
|
+
const url = authUrl ?? lastAuthUrl;
|
|
329
|
+
return [
|
|
330
|
+
"Zoho OAuth — authorize in your browser",
|
|
331
|
+
"",
|
|
332
|
+
"1. A browser window should open automatically.",
|
|
333
|
+
"2. Sign in to Zoho and approve access.",
|
|
334
|
+
"3. Return here — tools load when the callback succeeds.",
|
|
335
|
+
"",
|
|
336
|
+
url
|
|
337
|
+
? `Open this URL if the browser did not open:\n\n ${url}`
|
|
338
|
+
: "Waiting for authorize URL from Zoho…",
|
|
339
|
+
"",
|
|
340
|
+
"Timeout: 5 minutes. · Cancel: /zoho-disconnect",
|
|
341
|
+
].join("\n");
|
|
342
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve Zoho MCP URL from flags / env / config files.
|
|
3
|
+
*
|
|
4
|
+
* Precedence (high → low):
|
|
5
|
+
* 1. explicit url argument
|
|
6
|
+
* 2. --zoho-url / process flags
|
|
7
|
+
* 3. ZOHO_MCP_URL env
|
|
8
|
+
* 4. project .indusagi/zoho.json
|
|
9
|
+
* 5. profile ~/.indusagi/agent/zoho.json
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
|
|
13
|
+
import { homedir } from "node:os";
|
|
14
|
+
import { dirname, join } from "node:path";
|
|
15
|
+
import type { ZohoHostConfig } from "./types.js";
|
|
16
|
+
|
|
17
|
+
const ENV_URL = "ZOHO_MCP_URL";
|
|
18
|
+
const ENV_DISABLE = "ZOHO_DISABLED";
|
|
19
|
+
|
|
20
|
+
function readJsonFile(path: string): Record<string, unknown> | undefined {
|
|
21
|
+
try {
|
|
22
|
+
if (!existsSync(path)) return undefined;
|
|
23
|
+
const raw = readFileSync(path, "utf8");
|
|
24
|
+
const parsed = JSON.parse(raw) as unknown;
|
|
25
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
28
|
+
return parsed as Record<string, unknown>;
|
|
29
|
+
} catch {
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function configFromRecord(rec: Record<string, unknown> | undefined): ZohoHostConfig | undefined {
|
|
35
|
+
if (!rec) return undefined;
|
|
36
|
+
const url = typeof rec.url === "string" ? rec.url.trim() : "";
|
|
37
|
+
if (!url) return undefined;
|
|
38
|
+
const enabled = rec.enabled === false ? false : true;
|
|
39
|
+
const id = typeof rec.id === "string" && rec.id.trim() ? rec.id.trim() : "zoho";
|
|
40
|
+
const auth =
|
|
41
|
+
rec.auth === "none" || rec.auth === "oauth" || rec.auth === "auto"
|
|
42
|
+
? rec.auth
|
|
43
|
+
: "auto";
|
|
44
|
+
return { url, id, enabled, auth };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Profile path: ~/.indusagi/agent/zoho.json */
|
|
48
|
+
export function profileZohoPath(home = homedir()): string {
|
|
49
|
+
return join(home, ".indusagi", "agent", "zoho.json");
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Project path: <cwd>/.indusagi/zoho.json */
|
|
53
|
+
export function projectZohoPath(cwd: string): string {
|
|
54
|
+
return join(cwd, ".indusagi", "zoho.json");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function saveZohoConfig(
|
|
58
|
+
config: ZohoHostConfig,
|
|
59
|
+
where: "profile" | "project" = "profile",
|
|
60
|
+
cwd = process.cwd(),
|
|
61
|
+
): string {
|
|
62
|
+
const path = where === "project" ? projectZohoPath(cwd) : profileZohoPath();
|
|
63
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
64
|
+
const body = {
|
|
65
|
+
url: config.url,
|
|
66
|
+
id: config.id ?? "zoho",
|
|
67
|
+
enabled: config.enabled !== false,
|
|
68
|
+
auth: config.auth ?? "auto",
|
|
69
|
+
};
|
|
70
|
+
writeFileSync(path, JSON.stringify(body, null, 2) + "\n", "utf8");
|
|
71
|
+
return path;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export interface ResolveZohoOptions {
|
|
75
|
+
/** Explicit URL (e.g. from --zoho-url). */
|
|
76
|
+
url?: string;
|
|
77
|
+
/** Force disable (--no-zoho). */
|
|
78
|
+
disabled?: boolean;
|
|
79
|
+
/** Force enable when URL present (--zoho). */
|
|
80
|
+
forceEnable?: boolean;
|
|
81
|
+
cwd?: string;
|
|
82
|
+
env?: NodeJS.ProcessEnv;
|
|
83
|
+
/** Override home for profile path (tests). */
|
|
84
|
+
home?: string;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Resolve effective Zoho config or undefined if Zoho should not attach.
|
|
89
|
+
*/
|
|
90
|
+
export function resolveZohoConfig(opts: ResolveZohoOptions = {}): ZohoHostConfig | undefined {
|
|
91
|
+
const env = opts.env ?? process.env;
|
|
92
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
93
|
+
|
|
94
|
+
if (opts.disabled || env[ENV_DISABLE] === "1" || env[ENV_DISABLE] === "true") {
|
|
95
|
+
return undefined;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const fromArg = opts.url?.trim();
|
|
99
|
+
const fromEnv = env[ENV_URL]?.trim();
|
|
100
|
+
const fromProject = configFromRecord(readJsonFile(projectZohoPath(cwd)));
|
|
101
|
+
const fromProfile = configFromRecord(
|
|
102
|
+
readJsonFile(profileZohoPath(opts.home ?? homedir())),
|
|
103
|
+
);
|
|
104
|
+
const base =
|
|
105
|
+
(fromArg ? { url: fromArg, id: "zoho", enabled: true, auth: "auto" as const } : undefined) ??
|
|
106
|
+
(fromEnv ? { url: fromEnv, id: "zoho", enabled: true, auth: "auto" as const } : undefined) ??
|
|
107
|
+
fromProject ??
|
|
108
|
+
fromProfile;
|
|
109
|
+
|
|
110
|
+
if (!base) return undefined;
|
|
111
|
+
if (base.enabled === false && !opts.forceEnable) return undefined;
|
|
112
|
+
return {
|
|
113
|
+
...base,
|
|
114
|
+
enabled: true,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function redactUrl(url: string): string {
|
|
119
|
+
try {
|
|
120
|
+
const u = new URL(url);
|
|
121
|
+
return `${u.protocol}//${u.host}${u.pathname.slice(0, 24)}…`;
|
|
122
|
+
} catch {
|
|
123
|
+
return "(invalid url)";
|
|
124
|
+
}
|
|
125
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Zoho integration for indusagi-coding-agent.
|
|
3
|
+
*
|
|
4
|
+
* Consumes `indusagi/zoho` from the local framework package.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export type { ZohoHostConfig, ZohoAttachResult } from "./types.js";
|
|
8
|
+
export {
|
|
9
|
+
resolveZohoConfig,
|
|
10
|
+
saveZohoConfig,
|
|
11
|
+
profileZohoPath,
|
|
12
|
+
projectZohoPath,
|
|
13
|
+
redactUrl,
|
|
14
|
+
} from "./config.js";
|
|
15
|
+
export type { ResolveZohoOptions } from "./config.js";
|
|
16
|
+
export {
|
|
17
|
+
attachZohoTools,
|
|
18
|
+
loadZohoAgentTools,
|
|
19
|
+
disconnectLiveZoho,
|
|
20
|
+
getLiveZoho,
|
|
21
|
+
zohoStatusLines,
|
|
22
|
+
zohoOAuthPendingText,
|
|
23
|
+
isZohoConnecting,
|
|
24
|
+
getLastZohoAuthUrl,
|
|
25
|
+
hasUsableZohoTokens,
|
|
26
|
+
} from "./attach.js";
|
|
27
|
+
export type { AttachZohoOptions } from "./attach.js";
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Zoho host config for the coding agent.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export interface ZohoHostConfig {
|
|
6
|
+
/** Zoho MCP server URL from the Connect tab. */
|
|
7
|
+
readonly url: string;
|
|
8
|
+
/** Tool prefix / credential id (default: zoho). */
|
|
9
|
+
readonly id?: string;
|
|
10
|
+
/** When false, skip attach even if URL is set. */
|
|
11
|
+
readonly enabled?: boolean;
|
|
12
|
+
/** Auth mode for connectZohoMcp (default: auto). */
|
|
13
|
+
readonly auth?: "auto" | "oauth" | "none";
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface ZohoAttachResult {
|
|
17
|
+
readonly tools: readonly unknown[];
|
|
18
|
+
readonly toolNames: readonly string[];
|
|
19
|
+
readonly id: string;
|
|
20
|
+
close(): Promise<void>;
|
|
21
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { resolveZohoConfig, redactUrl } from "./config.js";
|
|
3
|
+
|
|
4
|
+
describe("resolveZohoConfig", () => {
|
|
5
|
+
const isolatedHome = "/tmp/indusagi-zoho-test-home-empty";
|
|
6
|
+
|
|
7
|
+
it("returns undefined when nothing is configured", () => {
|
|
8
|
+
expect(
|
|
9
|
+
resolveZohoConfig({ env: {}, cwd: "/tmp", home: isolatedHome, disabled: false }),
|
|
10
|
+
).toBeUndefined();
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it("prefers explicit url over env", () => {
|
|
14
|
+
const cfg = resolveZohoConfig({
|
|
15
|
+
url: "https://mcp.example/a",
|
|
16
|
+
env: { ZOHO_MCP_URL: "https://mcp.example/b" },
|
|
17
|
+
cwd: "/tmp",
|
|
18
|
+
home: isolatedHome,
|
|
19
|
+
});
|
|
20
|
+
expect(cfg?.url).toBe("https://mcp.example/a");
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("honors ZOHO_MCP_URL", () => {
|
|
24
|
+
const cfg = resolveZohoConfig({
|
|
25
|
+
env: { ZOHO_MCP_URL: "https://mcp.zoho.example/x" },
|
|
26
|
+
cwd: "/tmp",
|
|
27
|
+
home: isolatedHome,
|
|
28
|
+
});
|
|
29
|
+
expect(cfg?.url).toBe("https://mcp.zoho.example/x");
|
|
30
|
+
expect(cfg?.id).toBe("zoho");
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("disabled wins", () => {
|
|
34
|
+
const cfg = resolveZohoConfig({
|
|
35
|
+
url: "https://mcp.example/a",
|
|
36
|
+
disabled: true,
|
|
37
|
+
cwd: "/tmp",
|
|
38
|
+
home: isolatedHome,
|
|
39
|
+
});
|
|
40
|
+
expect(cfg).toBeUndefined();
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
describe("redactUrl", () => {
|
|
45
|
+
it("keeps host", () => {
|
|
46
|
+
const r = redactUrl("https://mcp.zoho.in/secret/path/more");
|
|
47
|
+
expect(r).toContain("mcp.zoho.in");
|
|
48
|
+
expect(r).toContain("…");
|
|
49
|
+
});
|
|
50
|
+
});
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Clipboard-image kit — pull a raster image off the OS clipboard and stage it
|
|
3
|
+
* on disk as a temp PNG, returning the path the composer can splice into a
|
|
4
|
+
* prompt.
|
|
5
|
+
*/
|
|
6
|
+
import { spawnSync } from "node:child_process";
|
|
7
|
+
import { readFileSync, writeFileSync } from "node:fs";
|
|
8
|
+
import { tmpdir } from "node:os";
|
|
9
|
+
import { join } from "node:path";
|
|
10
|
+
|
|
11
|
+
const READ_TIMEOUT_MS = 3000;
|
|
12
|
+
const MAX_BUFFER_BYTES = 50 * 1024 * 1024;
|
|
13
|
+
|
|
14
|
+
export interface ClipboardImageOptions {
|
|
15
|
+
readonly platform?: NodeJS.Platform;
|
|
16
|
+
readonly env?: NodeJS.ProcessEnv;
|
|
17
|
+
readonly dir?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function captureStdout(command: string, args: string[]): Buffer | null {
|
|
21
|
+
const result = spawnSync(command, [...args], {
|
|
22
|
+
timeout: READ_TIMEOUT_MS,
|
|
23
|
+
maxBuffer: MAX_BUFFER_BYTES,
|
|
24
|
+
});
|
|
25
|
+
if (result.error || result.status !== 0) return null;
|
|
26
|
+
const out = result.stdout;
|
|
27
|
+
if (!Buffer.isBuffer(out) || out.length === 0) return null;
|
|
28
|
+
return out;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function captureViaOsascript(): Buffer | null {
|
|
32
|
+
const result = spawnSync(
|
|
33
|
+
"osascript",
|
|
34
|
+
[
|
|
35
|
+
"-e",
|
|
36
|
+
"try",
|
|
37
|
+
"-e",
|
|
38
|
+
"set png to (the clipboard as \xABclass PNGf\xBB)",
|
|
39
|
+
"-e",
|
|
40
|
+
'set fp to (path to temporary items as string) & "indus-clip.png"',
|
|
41
|
+
"-e",
|
|
42
|
+
"set fh to open for access file fp with write permission",
|
|
43
|
+
"-e",
|
|
44
|
+
"set eof fh to 0",
|
|
45
|
+
"-e",
|
|
46
|
+
"write png to fh",
|
|
47
|
+
"-e",
|
|
48
|
+
"close access fh",
|
|
49
|
+
"-e",
|
|
50
|
+
"POSIX path of fp",
|
|
51
|
+
"-e",
|
|
52
|
+
"on error",
|
|
53
|
+
"-e",
|
|
54
|
+
'return ""',
|
|
55
|
+
"-e",
|
|
56
|
+
"end try",
|
|
57
|
+
],
|
|
58
|
+
{ timeout: READ_TIMEOUT_MS, encoding: "utf8" },
|
|
59
|
+
);
|
|
60
|
+
if (result.error || result.status !== 0) return null;
|
|
61
|
+
const path = String(result.stdout ?? "").trim();
|
|
62
|
+
if (path.length === 0) return null;
|
|
63
|
+
try {
|
|
64
|
+
const bytes = readFileSync(path);
|
|
65
|
+
return bytes.length > 0 ? bytes : null;
|
|
66
|
+
} catch {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function readClipboardBytes(platform: NodeJS.Platform, env: NodeJS.ProcessEnv): Buffer | null {
|
|
72
|
+
if (platform === "darwin") {
|
|
73
|
+
return captureStdout("pngpaste", ["-"]) ?? captureViaOsascript();
|
|
74
|
+
}
|
|
75
|
+
if (platform === "linux") {
|
|
76
|
+
const wayland =
|
|
77
|
+
Boolean(env.WAYLAND_DISPLAY) || env.XDG_SESSION_TYPE === "wayland";
|
|
78
|
+
if (wayland) {
|
|
79
|
+
const fromWayland = captureStdout("wl-paste", [
|
|
80
|
+
"--type",
|
|
81
|
+
"image/png",
|
|
82
|
+
"--no-newline",
|
|
83
|
+
]);
|
|
84
|
+
if (fromWayland) return fromWayland;
|
|
85
|
+
}
|
|
86
|
+
return captureStdout("xclip", ["-selection", "clipboard", "-t", "image/png", "-o"]);
|
|
87
|
+
}
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function readClipboardImage(options: ClipboardImageOptions = {}): string | null {
|
|
92
|
+
const platform = options.platform ?? process.platform;
|
|
93
|
+
const env = options.env ?? process.env;
|
|
94
|
+
const bytes = readClipboardBytes(platform, env);
|
|
95
|
+
if (bytes === null) return null;
|
|
96
|
+
const dir = options.dir ?? tmpdir();
|
|
97
|
+
const name = `indus-clipboard-${Date.now().toString(36)}-${Math.random()
|
|
98
|
+
.toString(36)
|
|
99
|
+
.slice(2, 8)}.png`;
|
|
100
|
+
const path = join(dir, name);
|
|
101
|
+
try {
|
|
102
|
+
writeFileSync(path, bytes);
|
|
103
|
+
return path;
|
|
104
|
+
} catch {
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
}
|