@fungi.computer/truffle 0.0.0-stage → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +294 -2
- package/computerd-loopback.cjs +16 -0
- package/dist/auth-cli.d.ts +36 -0
- package/dist/auth-cli.js +351 -0
- package/dist/fungi-account-command.d.ts +18 -0
- package/dist/fungi-account-command.js +92 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.js +17 -0
- package/dist/internal/private-workerd-sandbox-worker.mjs +27488 -0
- package/dist/internal/sandbox-metafile.json +6726 -0
- package/dist/linear-auth.d.ts +4 -0
- package/dist/linear-auth.js +72 -0
- package/dist/local-extensions.d.ts +97 -0
- package/dist/local-extensions.js +484 -0
- package/dist/local-login.d.ts +55 -0
- package/dist/local-login.js +167 -0
- package/dist/local-models.d.ts +14 -0
- package/dist/local-models.js +77 -0
- package/dist/local-skill-source.d.ts +6 -0
- package/dist/local-skill-source.js +357 -0
- package/dist/log-event.d.ts +2 -0
- package/dist/log-event.js +9 -0
- package/dist/private-bun-shiitake-host.d.ts +11 -0
- package/dist/private-bun-shiitake-host.js +66 -0
- package/dist/private-file-credential-store.d.ts +33 -0
- package/dist/private-file-credential-store.js +283 -0
- package/dist/private-files-patch.d.ts +3 -0
- package/dist/private-files-patch.js +86 -0
- package/dist/private-fungi-account.d.ts +235 -0
- package/dist/private-fungi-account.js +1149 -0
- package/dist/private-fungi-auth-error.d.ts +5 -0
- package/dist/private-fungi-auth-error.js +7 -0
- package/dist/private-fungi-mcp-connection.d.ts +22 -0
- package/dist/private-fungi-mcp-connection.js +143 -0
- package/dist/private-fungi-mcp-endpoint.d.ts +2 -0
- package/dist/private-fungi-mcp-endpoint.js +15 -0
- package/dist/private-fungi-mcp-selection.d.ts +14 -0
- package/dist/private-fungi-mcp-selection.js +36 -0
- package/dist/private-fungi-model-api.d.ts +6 -0
- package/dist/private-fungi-model-api.js +125 -0
- package/dist/private-fungi-model-catalog.d.ts +6 -0
- package/dist/private-fungi-model-catalog.js +78 -0
- package/dist/private-fungi-model-provider.d.ts +6 -0
- package/dist/private-fungi-model-provider.js +46 -0
- package/dist/private-fungi-pending-logouts.d.ts +34 -0
- package/dist/private-fungi-pending-logouts.js +136 -0
- package/dist/private-invocation-computer.d.ts +37 -0
- package/dist/private-invocation-computer.js +459 -0
- package/dist/private-invocation-files.d.ts +15 -0
- package/dist/private-invocation-files.js +449 -0
- package/dist/private-local-acp-stdio.d.ts +17 -0
- package/dist/private-local-acp-stdio.js +142 -0
- package/dist/private-local-auth-rpc.d.ts +69 -0
- package/dist/private-local-auth-rpc.js +273 -0
- package/dist/private-local-computer-attach.d.ts +49 -0
- package/dist/private-local-computer-attach.js +216 -0
- package/dist/private-local-computer-backend.d.ts +282 -0
- package/dist/private-local-computer-backend.js +402 -0
- package/dist/private-local-computer-processes.d.ts +199 -0
- package/dist/private-local-computer-processes.js +589 -0
- package/dist/private-local-default-model.d.ts +11 -0
- package/dist/private-local-default-model.js +45 -0
- package/dist/private-local-delegation.d.ts +29 -0
- package/dist/private-local-delegation.js +78 -0
- package/dist/private-local-effective-model.d.ts +19 -0
- package/dist/private-local-effective-model.js +14 -0
- package/dist/private-local-exec-launch.d.ts +41 -0
- package/dist/private-local-exec-launch.js +51 -0
- package/dist/private-local-exec-path.d.ts +6 -0
- package/dist/private-local-exec-path.js +40 -0
- package/dist/private-local-git-exclude.d.ts +2 -0
- package/dist/private-local-git-exclude.js +33 -0
- package/dist/private-local-http-dispatch.d.ts +5 -0
- package/dist/private-local-http-dispatch.js +14 -0
- package/dist/private-local-mail.d.ts +11 -0
- package/dist/private-local-mail.js +69 -0
- package/dist/private-local-model-settings.d.ts +5 -0
- package/dist/private-local-model-settings.js +31 -0
- package/dist/private-local-runtime-host.d.ts +141 -0
- package/dist/private-local-runtime-host.js +988 -0
- package/dist/private-local-service-contract.d.ts +90 -0
- package/dist/private-local-service-contract.js +1 -0
- package/dist/private-local-service-discovery.d.ts +101 -0
- package/dist/private-local-service-discovery.js +221 -0
- package/dist/private-local-service-ids.d.ts +3 -0
- package/dist/private-local-service-ids.js +8 -0
- package/dist/private-local-service-listener.d.ts +15 -0
- package/dist/private-local-service-listener.js +211 -0
- package/dist/private-local-service-owner.d.ts +1567 -0
- package/dist/private-local-service-owner.js +371 -0
- package/dist/private-local-service-process.d.ts +1 -0
- package/dist/private-local-service-process.js +6 -0
- package/dist/private-local-service.d.ts +17 -0
- package/dist/private-local-service.js +330 -0
- package/dist/private-local-session-discovery.d.ts +19 -0
- package/dist/private-local-session-discovery.js +157 -0
- package/dist/private-local-shiitake-host.d.ts +44 -0
- package/dist/private-local-shiitake-host.js +143 -0
- package/dist/private-local-subagents.d.ts +27 -0
- package/dist/private-local-subagents.js +77 -0
- package/dist/private-local-worker-server.d.ts +15 -0
- package/dist/private-local-worker-server.js +136 -0
- package/dist/private-sqlite-owner.d.ts +7 -0
- package/dist/private-sqlite-owner.js +103 -0
- package/dist/private-team-computers.d.ts +38 -0
- package/dist/private-team-computers.js +101 -0
- package/dist/private-workerd-sandbox.d.ts +3 -0
- package/dist/private-workerd-sandbox.js +347 -0
- package/dist/team-apps.d.ts +24 -0
- package/dist/team-apps.js +123 -0
- package/dist/tracker-git-credential.d.ts +15 -0
- package/dist/tracker-git-credential.js +87 -0
- package/native/bin/build.json +4037 -0
- package/native/bin/fungi-apply-patch +0 -0
- package/package.json +148 -3
package/dist/auth-cli.js
ADDED
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Readable, Writable } from "node:stream";
|
|
3
|
+
import { Cli } from "@fungi.computer/lantern";
|
|
4
|
+
import { linearAuthInteraction } from "./linear-auth.js";
|
|
5
|
+
export { linearAuthInteraction } from "./linear-auth.js";
|
|
6
|
+
import { parseArgs } from "node:util";
|
|
7
|
+
import { localLoginProviders } from "./local-login.js";
|
|
8
|
+
import { createPrivateModels } from "./local-models.js";
|
|
9
|
+
import { createPrivateCredentialStore } from "./private-file-credential-store.js";
|
|
10
|
+
export { maximumCredentialStringLength as localAuthInputLimit } from "./private-file-credential-store.js";
|
|
11
|
+
function usage() {
|
|
12
|
+
return "Usage: fungi-local-auth <login|status|logout> [--provider <id>] [--auth-type oauth|api_key] [--method <id>]";
|
|
13
|
+
}
|
|
14
|
+
function parseAuthType(value) {
|
|
15
|
+
if (value === undefined)
|
|
16
|
+
return undefined;
|
|
17
|
+
if (value === "oauth" || value === "api_key")
|
|
18
|
+
return value;
|
|
19
|
+
throw new Error("--auth-type must be oauth or api_key");
|
|
20
|
+
}
|
|
21
|
+
function writeMessage(output, interaction, message) {
|
|
22
|
+
if (interaction !== undefined)
|
|
23
|
+
interaction.notify({ type: "info", message });
|
|
24
|
+
else
|
|
25
|
+
output.write(`${message}\n`);
|
|
26
|
+
}
|
|
27
|
+
function providerLabel(provider) {
|
|
28
|
+
return provider.name === provider.id
|
|
29
|
+
? provider.name
|
|
30
|
+
: `${provider.name} (${provider.id})`;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Interactive login methods for one provider, from the same registry the
|
|
34
|
+
* Whistle/TUI picker uses (`localLoginProviders`), so Pi's labels and which
|
|
35
|
+
* methods count as interactive are decided in exactly one place.
|
|
36
|
+
*/
|
|
37
|
+
function authMethodOptions(provider) {
|
|
38
|
+
const [entry] = localLoginProviders([provider]);
|
|
39
|
+
if (entry === undefined)
|
|
40
|
+
return [];
|
|
41
|
+
return entry.methods.map((method) => ({
|
|
42
|
+
id: method.kind === "subscription"
|
|
43
|
+
? "oauth"
|
|
44
|
+
: "api_key",
|
|
45
|
+
label: method.label,
|
|
46
|
+
}));
|
|
47
|
+
}
|
|
48
|
+
/** Whether `provider` declares `requested` at all, interactive or not. */
|
|
49
|
+
function declaresAuthType(provider, requested) {
|
|
50
|
+
return requested === "oauth"
|
|
51
|
+
? provider.auth.oauth !== undefined
|
|
52
|
+
: provider.auth.apiKey !== undefined;
|
|
53
|
+
}
|
|
54
|
+
function terminalInteraction(input, output) {
|
|
55
|
+
const cli = new Cli({
|
|
56
|
+
brand: "fungi-local-auth",
|
|
57
|
+
input: input instanceof Readable ? input : Readable.from(input),
|
|
58
|
+
output: output instanceof Writable
|
|
59
|
+
? output
|
|
60
|
+
: new Writable({
|
|
61
|
+
write(chunk, _encoding, callback) {
|
|
62
|
+
output.write(chunk);
|
|
63
|
+
callback();
|
|
64
|
+
},
|
|
65
|
+
}),
|
|
66
|
+
});
|
|
67
|
+
return linearAuthInteraction(cli.interactive, cli);
|
|
68
|
+
}
|
|
69
|
+
function interactionFor(signal, input, output, supplied, method,
|
|
70
|
+
// Known once `login()` resolves the provider, before Pi's own login()
|
|
71
|
+
// runs; stamped onto a forwarded `auth_url` event so linear-auth.ts can
|
|
72
|
+
// head it "Sign in to <provider>" instead of a bare "Sign in".
|
|
73
|
+
providerLabel) {
|
|
74
|
+
let methodOverride = method;
|
|
75
|
+
return {
|
|
76
|
+
signal,
|
|
77
|
+
prompt: async (prompt) => {
|
|
78
|
+
if (prompt.type === "select" && methodOverride !== undefined) {
|
|
79
|
+
const selected = prompt.options.find((option) => option.id === methodOverride);
|
|
80
|
+
if (selected === undefined)
|
|
81
|
+
throw new Error(`Pi auth did not offer method ${methodOverride}`);
|
|
82
|
+
methodOverride = undefined;
|
|
83
|
+
return selected.id;
|
|
84
|
+
}
|
|
85
|
+
signal.throwIfAborted();
|
|
86
|
+
const promptSignal = prompt.signal === undefined
|
|
87
|
+
? signal
|
|
88
|
+
: AbortSignal.any([signal, prompt.signal]);
|
|
89
|
+
return (supplied ?? terminalInteraction(input, output)).prompt({
|
|
90
|
+
...prompt,
|
|
91
|
+
signal: promptSignal,
|
|
92
|
+
});
|
|
93
|
+
},
|
|
94
|
+
notify: (event) => {
|
|
95
|
+
const augmented = providerLabel === undefined || event.type !== "auth_url"
|
|
96
|
+
? event
|
|
97
|
+
: { ...event, providerLabel };
|
|
98
|
+
if (supplied === undefined)
|
|
99
|
+
terminalInteraction(input, output).notify(augmented);
|
|
100
|
+
else
|
|
101
|
+
supplied.notify(augmented);
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
async function chooseProvider(providers, requested, signal, input, output, supplied, message = "Choose a provider") {
|
|
106
|
+
if (requested !== undefined) {
|
|
107
|
+
const provider = providers.find((candidate) => candidate.id === requested);
|
|
108
|
+
if (provider === undefined)
|
|
109
|
+
throw new Error(`Unknown provider: ${requested}`);
|
|
110
|
+
return provider;
|
|
111
|
+
}
|
|
112
|
+
const id = await interactionFor(signal, input, output, supplied).prompt({
|
|
113
|
+
type: "select",
|
|
114
|
+
message,
|
|
115
|
+
options: providers.map((provider) => ({
|
|
116
|
+
id: provider.id,
|
|
117
|
+
label: providerLabel(provider),
|
|
118
|
+
})),
|
|
119
|
+
});
|
|
120
|
+
const provider = providers.find((candidate) => candidate.id === id);
|
|
121
|
+
if (provider === undefined)
|
|
122
|
+
throw new Error(`Unknown provider: ${id}`);
|
|
123
|
+
return provider;
|
|
124
|
+
}
|
|
125
|
+
async function chooseAuthType(provider, requested, signal, input, output, supplied) {
|
|
126
|
+
if (requested !== undefined) {
|
|
127
|
+
if (!declaresAuthType(provider, requested))
|
|
128
|
+
throw new Error(`${providerLabel(provider)} does not offer ${requested} login`);
|
|
129
|
+
return requested;
|
|
130
|
+
}
|
|
131
|
+
const options = authMethodOptions(provider);
|
|
132
|
+
if (options.length === 0) {
|
|
133
|
+
// No interactive method (e.g. an ambient-only API key): fall through to
|
|
134
|
+
// api_key so login() can explain why, the same as an explicit request.
|
|
135
|
+
if (provider.auth.apiKey !== undefined)
|
|
136
|
+
return "api_key";
|
|
137
|
+
throw new Error(`${providerLabel(provider)} has no login method`);
|
|
138
|
+
}
|
|
139
|
+
if (options.length === 1) {
|
|
140
|
+
const only = options[0];
|
|
141
|
+
if (only === undefined)
|
|
142
|
+
throw new Error("No login method is available");
|
|
143
|
+
return only.id;
|
|
144
|
+
}
|
|
145
|
+
const id = await interactionFor(signal, input, output, supplied).prompt({
|
|
146
|
+
type: "select",
|
|
147
|
+
message: `Choose login method for ${providerLabel(provider)}`,
|
|
148
|
+
options,
|
|
149
|
+
});
|
|
150
|
+
const selected = options.find((option) => option.id === id);
|
|
151
|
+
if (selected === undefined)
|
|
152
|
+
throw new Error(`Unknown login method: ${id}`);
|
|
153
|
+
return selected.id;
|
|
154
|
+
}
|
|
155
|
+
async function login(models, command, signal, input, output, getDeviceId, supplied) {
|
|
156
|
+
const provider = await chooseProvider(models.getProviders(), command.provider, signal, input, output, supplied);
|
|
157
|
+
const authType = await chooseAuthType(provider, command.authType, signal, input, output, supplied);
|
|
158
|
+
if (authType === "api_key" && provider.auth.apiKey?.login === undefined) {
|
|
159
|
+
const message = `${providerLabel(provider)} uses ambient credentials; configure its environment or provider settings. No credential was changed.`;
|
|
160
|
+
writeMessage(output, supplied, message);
|
|
161
|
+
throw new Error(message);
|
|
162
|
+
}
|
|
163
|
+
const signInLabel = authType === "oauth" && provider.id === "openai"
|
|
164
|
+
? "ChatGPT for Shiitake"
|
|
165
|
+
: authType === "oauth" && provider.id === "anthropic"
|
|
166
|
+
? "Claude for Shiitake"
|
|
167
|
+
: provider.name;
|
|
168
|
+
const interaction = interactionFor(signal, input, output, supplied, command.method, signInLabel);
|
|
169
|
+
// Sign in with ChatGPT needs a stable installation UUID in its authorize
|
|
170
|
+
// URL (`ext_agent_host_id`); pass it at this one call site, the way Pi's
|
|
171
|
+
// own coding-agent does, instead of patching Pi's `models.login`. Pi's
|
|
172
|
+
// option is synchronous, so resolve our (async, disk-backed) device ID
|
|
173
|
+
// first and hand it a closure over the resolved value.
|
|
174
|
+
if (provider.id === "openai" && authType === "oauth") {
|
|
175
|
+
signal.throwIfAborted();
|
|
176
|
+
const deviceId = await getDeviceId();
|
|
177
|
+
signal.throwIfAborted();
|
|
178
|
+
await models.login(provider.id, authType, interaction, {
|
|
179
|
+
getDeviceId: () => deviceId,
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
else {
|
|
183
|
+
await models.login(provider.id, authType, interaction);
|
|
184
|
+
}
|
|
185
|
+
writeMessage(output, supplied, `${signInLabel} login complete.`);
|
|
186
|
+
}
|
|
187
|
+
const credentialLabel = { oauth: "subscription", api_key: "API key" };
|
|
188
|
+
/**
|
|
189
|
+
* Signed-in providers first, then one summary line; the full catalog only
|
|
190
|
+
* when one provider is asked for by name.
|
|
191
|
+
*/
|
|
192
|
+
function statusLines(statuses, explicit) {
|
|
193
|
+
const label = (status) => status.providerName === status.providerId
|
|
194
|
+
? status.providerName
|
|
195
|
+
: `${status.providerName} (${status.providerId})`;
|
|
196
|
+
const signedIn = statuses.flatMap((status) => status.state === "configured"
|
|
197
|
+
? [`${label(status)}: signed in (${credentialLabel[status.type]})`]
|
|
198
|
+
: []);
|
|
199
|
+
if (explicit)
|
|
200
|
+
return [
|
|
201
|
+
...signedIn,
|
|
202
|
+
...statuses.flatMap((status) => status.state === "unconfigured"
|
|
203
|
+
? [`${label(status)}: not signed in`]
|
|
204
|
+
: []),
|
|
205
|
+
];
|
|
206
|
+
const others = statuses.length - signedIn.length;
|
|
207
|
+
return signedIn.length === 0
|
|
208
|
+
? [
|
|
209
|
+
`Model providers: none signed in (${String(others)} available). Run fungi login.`,
|
|
210
|
+
]
|
|
211
|
+
: [
|
|
212
|
+
...signedIn,
|
|
213
|
+
`${String(others)} other providers not signed in. Run fungi login to add one.`,
|
|
214
|
+
];
|
|
215
|
+
}
|
|
216
|
+
async function status(models, providerId, signal, output, interaction) {
|
|
217
|
+
const providers = models.getProviders();
|
|
218
|
+
const selected = providerId === undefined
|
|
219
|
+
? providers
|
|
220
|
+
: providers.filter((provider) => provider.id === providerId);
|
|
221
|
+
if (selected.length === 0)
|
|
222
|
+
throw new Error(`Unknown provider: ${providerId ?? "(all providers)"}`);
|
|
223
|
+
const statuses = [];
|
|
224
|
+
for (const provider of selected) {
|
|
225
|
+
const checked = await models.checkAuth(provider.id, { signal });
|
|
226
|
+
statuses.push(checked === undefined
|
|
227
|
+
? {
|
|
228
|
+
providerId: provider.id,
|
|
229
|
+
providerName: provider.name,
|
|
230
|
+
state: "unconfigured",
|
|
231
|
+
}
|
|
232
|
+
: {
|
|
233
|
+
providerId: provider.id,
|
|
234
|
+
providerName: provider.name,
|
|
235
|
+
state: "configured",
|
|
236
|
+
type: checked.type,
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
for (const line of statusLines(statuses, providerId !== undefined))
|
|
240
|
+
writeMessage(output, interaction, line);
|
|
241
|
+
return {
|
|
242
|
+
kind: "status",
|
|
243
|
+
providers: Object.freeze(statuses.map((providerStatus) => Object.freeze(providerStatus))),
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
async function signedInProviders(models, providers, signal) {
|
|
247
|
+
const signedIn = [];
|
|
248
|
+
for (const provider of providers)
|
|
249
|
+
if ((await models.checkAuth(provider.id, { signal })) !== undefined)
|
|
250
|
+
signedIn.push(provider);
|
|
251
|
+
return signedIn;
|
|
252
|
+
}
|
|
253
|
+
async function runCommand(command, models, signal, input, output, getDeviceId, interaction) {
|
|
254
|
+
if (command.type === "status")
|
|
255
|
+
return status(models, command.provider, signal, output, interaction);
|
|
256
|
+
if (command.type === "login") {
|
|
257
|
+
await login(models, command, signal, input, output, getDeviceId, interaction);
|
|
258
|
+
return { kind: "completed" };
|
|
259
|
+
}
|
|
260
|
+
const providers = models.getProviders();
|
|
261
|
+
const candidates = command.provider === undefined
|
|
262
|
+
? await signedInProviders(models, providers, signal)
|
|
263
|
+
: providers;
|
|
264
|
+
if (candidates.length === 0) {
|
|
265
|
+
writeMessage(output, interaction, "You're not signed in to any model provider. Nothing to log out of.");
|
|
266
|
+
return { kind: "completed" };
|
|
267
|
+
}
|
|
268
|
+
const provider = await chooseProvider(candidates, command.provider, signal, input, output, interaction, "Log out of which provider?");
|
|
269
|
+
await models.logout(provider.id, { signal });
|
|
270
|
+
writeMessage(output, interaction, `${providerLabel(provider)} logout complete.`);
|
|
271
|
+
return { kind: "completed" };
|
|
272
|
+
}
|
|
273
|
+
function parseCommand(argv) {
|
|
274
|
+
const parsed = parseArgs({
|
|
275
|
+
args: [...argv],
|
|
276
|
+
allowPositionals: true,
|
|
277
|
+
strict: true,
|
|
278
|
+
options: {
|
|
279
|
+
help: { type: "boolean", short: "h" },
|
|
280
|
+
provider: { type: "string" },
|
|
281
|
+
"auth-type": { type: "string" },
|
|
282
|
+
method: { type: "string" },
|
|
283
|
+
},
|
|
284
|
+
});
|
|
285
|
+
const command = parsed.positionals[0] ?? "help";
|
|
286
|
+
if (parsed.values.help === true || command === "help")
|
|
287
|
+
return { type: "help" };
|
|
288
|
+
if (parsed.positionals.length !== 1)
|
|
289
|
+
throw new Error(usage());
|
|
290
|
+
const provider = parsed.values.provider;
|
|
291
|
+
const method = parsed.values.method;
|
|
292
|
+
const authType = parseAuthType(parsed.values["auth-type"]);
|
|
293
|
+
if (command === "login") {
|
|
294
|
+
const result = { type: "login" };
|
|
295
|
+
if (provider !== undefined)
|
|
296
|
+
result.provider = provider;
|
|
297
|
+
if (authType !== undefined)
|
|
298
|
+
result.authType = authType;
|
|
299
|
+
if (method !== undefined)
|
|
300
|
+
result.method = method === "device-code" ? "device_code" : method;
|
|
301
|
+
return result;
|
|
302
|
+
}
|
|
303
|
+
if (method !== undefined || authType !== undefined)
|
|
304
|
+
throw new Error("--auth-type and --method apply only to login");
|
|
305
|
+
if (command === "status" || command === "logout") {
|
|
306
|
+
const result = {
|
|
307
|
+
type: command,
|
|
308
|
+
};
|
|
309
|
+
if (provider !== undefined)
|
|
310
|
+
result.provider = provider;
|
|
311
|
+
return result;
|
|
312
|
+
}
|
|
313
|
+
throw new Error(usage());
|
|
314
|
+
}
|
|
315
|
+
export async function runLocalAuthCli(argv, options = {}) {
|
|
316
|
+
const output = options.output ?? process.stdout;
|
|
317
|
+
const command = parseCommand(argv);
|
|
318
|
+
if (command.type === "help") {
|
|
319
|
+
output.write(`${usage()}\n`);
|
|
320
|
+
return { kind: "completed" };
|
|
321
|
+
}
|
|
322
|
+
const controller = new AbortController();
|
|
323
|
+
const signals = options.signals ?? process;
|
|
324
|
+
const abort = () => {
|
|
325
|
+
controller.abort();
|
|
326
|
+
};
|
|
327
|
+
signals.once("SIGINT", abort);
|
|
328
|
+
signals.once("SIGTERM", abort);
|
|
329
|
+
const getDeviceId = options.getDeviceId ?? (() => createPrivateCredentialStore().getDeviceId());
|
|
330
|
+
try {
|
|
331
|
+
return await runCommand(command, options.models ?? createPrivateModels(), options.interaction?.signal === undefined
|
|
332
|
+
? controller.signal
|
|
333
|
+
: AbortSignal.any([controller.signal, options.interaction.signal]), options.input ?? process.stdin, output, getDeviceId, options.interaction);
|
|
334
|
+
}
|
|
335
|
+
finally {
|
|
336
|
+
signals.removeListener("SIGINT", abort);
|
|
337
|
+
signals.removeListener("SIGTERM", abort);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
if (import.meta.main) {
|
|
341
|
+
const cli = new Cli({ brand: "fungi-local-auth" });
|
|
342
|
+
if (cli.interactive)
|
|
343
|
+
cli.welcome("Provider login for your local agents.");
|
|
344
|
+
runLocalAuthCli(process.argv.slice(2), {
|
|
345
|
+
interaction: linearAuthInteraction(cli.interactive, cli),
|
|
346
|
+
}).catch((cause) => {
|
|
347
|
+
// Provider messages can contain credentials; keep the diagnostic bounded.
|
|
348
|
+
process.stderr.write(`Local authentication command failed (${cause instanceof Error ? cause.name : "NonError"}).\n`);
|
|
349
|
+
process.exitCode = 1;
|
|
350
|
+
});
|
|
351
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { FetchLike } from "@modelcontextprotocol/client";
|
|
2
|
+
export type FungiAccountCommand = Readonly<{
|
|
3
|
+
action: "status" | "teams" | "balance";
|
|
4
|
+
}> | Readonly<{
|
|
5
|
+
action: "use-team";
|
|
6
|
+
teamId: string;
|
|
7
|
+
}>;
|
|
8
|
+
export type FungiAccountCommandOptions = Readonly<{
|
|
9
|
+
json: boolean;
|
|
10
|
+
stdout: Readonly<{
|
|
11
|
+
write: (text: string) => void;
|
|
12
|
+
}>;
|
|
13
|
+
credentialPath?: string | undefined;
|
|
14
|
+
fetch?: FetchLike | undefined;
|
|
15
|
+
}>;
|
|
16
|
+
export declare const fungiAccountCommandHelp = "Usage: fungi account [--json]\n fungi account teams [--json]\n fungi account use-team <team-id>\n fungi account balance [--json]\n\nRead the signed-in Fungi account without exposing its credentials. The plain\ncommand shows the Hub and the Team that pays for models; teams lists your\ncurrent memberships; use-team selects one of them (the prior Team is kept when\nyou are not a current member); balance reads that Team's credit when you may\nsee its billing.\n";
|
|
17
|
+
/** `fungi account`: non-secret reads over Local's one Fungi account owner. */
|
|
18
|
+
export declare function runFungiAccountCommand(command: FungiAccountCommand, options: FungiAccountCommandOptions): Promise<void>;
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { readFungiAccountStatus, readFungiAccountTeams, readFungiTeamBalance, switchFungiAccountTeam, } from "./private-fungi-account.js";
|
|
2
|
+
export const fungiAccountCommandHelp = `Usage: fungi account [--json]
|
|
3
|
+
fungi account teams [--json]
|
|
4
|
+
fungi account use-team <team-id>
|
|
5
|
+
fungi account balance [--json]
|
|
6
|
+
|
|
7
|
+
Read the signed-in Fungi account without exposing its credentials. The plain
|
|
8
|
+
command shows the Hub and the Team that pays for models; teams lists your
|
|
9
|
+
current memberships; use-team selects one of them (the prior Team is kept when
|
|
10
|
+
you are not a current member); balance reads that Team's credit when you may
|
|
11
|
+
see its billing.
|
|
12
|
+
`;
|
|
13
|
+
function teamFields({ teamId, handle, name }) {
|
|
14
|
+
return { teamId, handle, name };
|
|
15
|
+
}
|
|
16
|
+
/** `fungi account`: non-secret reads over Local's one Fungi account owner. */
|
|
17
|
+
export async function runFungiAccountCommand(command, options) {
|
|
18
|
+
const owner = {};
|
|
19
|
+
if (options.credentialPath !== undefined)
|
|
20
|
+
owner.credentialPath = options.credentialPath;
|
|
21
|
+
if (options.fetch !== undefined)
|
|
22
|
+
owner.fetch = options.fetch;
|
|
23
|
+
const emit = (reply, text) => {
|
|
24
|
+
options.stdout.write(options.json
|
|
25
|
+
? `${JSON.stringify({ schemaVersion: 1, ...reply })}\n`
|
|
26
|
+
: `${text}\n`);
|
|
27
|
+
};
|
|
28
|
+
if (command.action === "use-team") {
|
|
29
|
+
await switchFungiAccountTeam(command.teamId, owner);
|
|
30
|
+
const team = (await readFungiAccountStatus(owner))?.team;
|
|
31
|
+
emit({ team: team === undefined ? undefined : teamFields(team) }, `Fungi models now bill ${team?.name ?? command.teamId}.`);
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
const status = await readFungiAccountStatus(owner);
|
|
35
|
+
if (status === undefined) {
|
|
36
|
+
emit({ signedIn: false }, "Not signed in to Fungi. Run fungi login --provider fungi.");
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
if (command.action === "teams") {
|
|
40
|
+
const { teams, selectedTeamId } = await readFungiAccountTeams(owner);
|
|
41
|
+
emit({
|
|
42
|
+
teams: teams.map((team) => ({
|
|
43
|
+
...teamFields(team),
|
|
44
|
+
selected: team.teamId === selectedTeamId,
|
|
45
|
+
})),
|
|
46
|
+
}, teams
|
|
47
|
+
.map((team) => `${team.teamId === selectedTeamId ? "*" : " "} ${team.name} (${team.handle}) ${team.teamId}`)
|
|
48
|
+
.join("\n") || "You are not a member of any Team.");
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
if (command.action === "balance") {
|
|
52
|
+
const balance = await readFungiTeamBalance(owner);
|
|
53
|
+
emit(balanceReply(balance), balanceText(balance));
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
const reply = {
|
|
57
|
+
signedIn: true,
|
|
58
|
+
hub: status.issuer,
|
|
59
|
+
accessExpiresAt: status.accessExpiresAt.toISOString(),
|
|
60
|
+
};
|
|
61
|
+
if (status.team !== undefined)
|
|
62
|
+
reply.team = teamFields(status.team);
|
|
63
|
+
if (status.teamCount !== undefined)
|
|
64
|
+
reply.teamCount = status.teamCount;
|
|
65
|
+
emit(reply, `Signed in to ${status.issuer}. ${status.team === undefined ? "No Team selected for Fungi models." : `Fungi models bill ${status.team.name} (${status.team.handle}).`}`);
|
|
66
|
+
}
|
|
67
|
+
function balanceReply(balance) {
|
|
68
|
+
if (balance.kind === "not-available")
|
|
69
|
+
return {
|
|
70
|
+
kind: balance.kind,
|
|
71
|
+
reason: balance.reason,
|
|
72
|
+
};
|
|
73
|
+
const { teamId, ...credit } = balance.credit;
|
|
74
|
+
return { kind: balance.kind, teamId, credit };
|
|
75
|
+
}
|
|
76
|
+
function dollars(money) {
|
|
77
|
+
const cents = BigInt(money.amount) / 10n ** BigInt(money.scale - 2);
|
|
78
|
+
const whole = cents / 100n;
|
|
79
|
+
const rest = (cents % 100n).toString().padStart(2, "0");
|
|
80
|
+
return `$${whole.toString()}.${rest}`;
|
|
81
|
+
}
|
|
82
|
+
function balanceText(balance) {
|
|
83
|
+
if (balance.kind === "balance")
|
|
84
|
+
return `Available ${dollars(balance.credit.available)} (purchased ${dollars(balance.credit.balance)}, reserved ${dollars(balance.credit.reserved)}${balance.credit.included === null ? "" : `, included ${dollars(balance.credit.included.remaining)} left`}).`;
|
|
85
|
+
if (balance.reason === "no-team")
|
|
86
|
+
return "No Team selected. Run fungi account use-team <team-id>.";
|
|
87
|
+
if (balance.reason === "not-a-member")
|
|
88
|
+
return "You are not a current member of the selected Team. Run fungi account teams.";
|
|
89
|
+
if (balance.reason === "sign-in-again")
|
|
90
|
+
return "This sign-in lacks billing permission. Run fungi login --provider fungi again.";
|
|
91
|
+
return "This Fungi Hub does not admit Local sign-ins for Team balance.";
|
|
92
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export { createPrivateCredentialStore, resolveDefaultCredentialPath, } from "./private-file-credential-store.js";
|
|
2
|
+
export type { PrivateCredentialStore } from "./private-file-credential-store.js";
|
|
3
|
+
export { createPrivateModels, registerPrivateBundledModelOAuthFlows, listPrivateModels, type PrivateLocalModelChoice, } from "./local-models.js";
|
|
4
|
+
export { defaultLocalModel } from "./private-local-default-model.js";
|
|
5
|
+
export { createPrivateInvocationFilesPort } from "./private-invocation-files.js";
|
|
6
|
+
export { createPrivateInvocationComputer } from "./private-invocation-computer.js";
|
|
7
|
+
export { ensurePrivateLocalGitExclude } from "./private-local-git-exclude.js";
|
|
8
|
+
export { createPrivateWorkerdSandbox } from "./private-workerd-sandbox.js";
|
|
9
|
+
export { signInFungiAccount, launchPlatformBrowser, signOutFungiAccount, readFungiAccountStatus, switchFungiAccountTeam, freshFungiAccess, createFungiAccountOAuth, createFungiAccountCredentialStore, type FungiAccountTeam, type PrivateFungiAccountLoginOptions, type PrivateFungiAccountLogoutResult, } from "./private-fungi-account.js";
|
|
10
|
+
export { fungiAccountCommandHelp, runFungiAccountCommand, type FungiAccountCommand, } from "./fungi-account-command.js";
|
|
11
|
+
export type { PrivateFungiMcpConnectionOptions } from "./private-fungi-mcp-connection.js";
|
|
12
|
+
export { parseFungiMcpEndpoint } from "./private-fungi-mcp-endpoint.js";
|
|
13
|
+
export { attachPrivateLocalComputer, detachPrivateLocalComputer, listPrivateLocalComputers, runPrivateLocalComputer, type PrivateLocalComputerAttachResult, type PrivateLocalComputerDetachResult, } from "./private-local-computer-attach.js";
|
|
14
|
+
export { openPrivateLocalRuntimeHost, type PrivateLocalRuntimeHost, type PrivateLocalRuntimeHostOptions, } from "./private-local-runtime-host.js";
|
|
15
|
+
export { openPrivateLocalShiitakeHost, openPrivateLocalShiitakeWorkerBatch, type PrivateLocalShiitakeHost, type PrivateLocalShiitakeHostOpener, type PrivateLocalShiitakeServer, type PrivateLocalShiitakeServerOpener, type PrivateLocalShiitakeWorkerBatch, type PrivateLocalShiitakeWorkerInput, type PrivateLocalShiitakeWorkerRoute, } from "./private-local-shiitake-host.js";
|
|
16
|
+
export { createPrivateLocalServiceManager, LocalSessionPathConflict, runPrivateLocalWorkerServer, type PrivateLocalServiceConnection, type PrivateLocalServiceManager, type PrivateLocalServiceRequestOptions, type PrivateLocalServiceSelection, type PrivateLocalServiceStatus, type PrivateLocalSessionChoice, type PrivateLocalSessionDiscovery, type PrivateLocalSessionInspection, type PrivateLocalSessionProblem, type PrivateLocalWorkerServerIo, } from "./private-local-service.js";
|
|
17
|
+
export { createPrivateLocalAcpAgent, runPrivateLocalAcpStdio, type LocalAcpStdioOptions, } from "./private-local-acp-stdio.js";
|
|
18
|
+
export { listPrivateTeamComputers, startPrivateTeamComputer, } from "./private-team-computers.js";
|
|
19
|
+
export { runTrackerGitCredential } from "./tracker-git-credential.js";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export { createPrivateCredentialStore, resolveDefaultCredentialPath, } from "./private-file-credential-store.js";
|
|
2
|
+
export { createPrivateModels, registerPrivateBundledModelOAuthFlows, listPrivateModels, } from "./local-models.js";
|
|
3
|
+
export { defaultLocalModel } from "./private-local-default-model.js";
|
|
4
|
+
export { createPrivateInvocationFilesPort } from "./private-invocation-files.js";
|
|
5
|
+
export { createPrivateInvocationComputer } from "./private-invocation-computer.js";
|
|
6
|
+
export { ensurePrivateLocalGitExclude } from "./private-local-git-exclude.js";
|
|
7
|
+
export { createPrivateWorkerdSandbox } from "./private-workerd-sandbox.js";
|
|
8
|
+
export { signInFungiAccount, launchPlatformBrowser, signOutFungiAccount, readFungiAccountStatus, switchFungiAccountTeam, freshFungiAccess, createFungiAccountOAuth, createFungiAccountCredentialStore, } from "./private-fungi-account.js";
|
|
9
|
+
export { fungiAccountCommandHelp, runFungiAccountCommand, } from "./fungi-account-command.js";
|
|
10
|
+
export { parseFungiMcpEndpoint } from "./private-fungi-mcp-endpoint.js";
|
|
11
|
+
export { attachPrivateLocalComputer, detachPrivateLocalComputer, listPrivateLocalComputers, runPrivateLocalComputer, } from "./private-local-computer-attach.js";
|
|
12
|
+
export { openPrivateLocalRuntimeHost, } from "./private-local-runtime-host.js";
|
|
13
|
+
export { openPrivateLocalShiitakeHost, openPrivateLocalShiitakeWorkerBatch, } from "./private-local-shiitake-host.js";
|
|
14
|
+
export { createPrivateLocalServiceManager, LocalSessionPathConflict, runPrivateLocalWorkerServer, } from "./private-local-service.js";
|
|
15
|
+
export { createPrivateLocalAcpAgent, runPrivateLocalAcpStdio, } from "./private-local-acp-stdio.js";
|
|
16
|
+
export { listPrivateTeamComputers, startPrivateTeamComputer, } from "./private-team-computers.js";
|
|
17
|
+
export { runTrackerGitCredential } from "./tracker-git-credential.js";
|