@audiofool/moondog 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.
- package/README.md +125 -0
- package/assets/brand/moondog-lunar-record/moondog-logo-1x1.png +0 -0
- package/contracts/v1/capability-policy.schema.json +572 -0
- package/contracts/v1/definitions.schema.json +365 -0
- package/contracts/v1/library-track-observation.schema.json +138 -0
- package/contracts/v1/listening-event.schema.json +111 -0
- package/contracts/v1/profile-evidence.schema.json +237 -0
- package/contracts/v1/taste-event.schema.json +258 -0
- package/contracts/v1/tool-invocation.schema.json +318 -0
- package/contracts/v1/tool-receipt.schema.json +1066 -0
- package/contracts/v1/track-ref.schema.json +275 -0
- package/contracts/v2/profile-evidence.schema.json +284 -0
- package/package.json +105 -0
- package/scripts/contract-lib.mjs +361 -0
- package/scripts/contract-semantics.mjs +2630 -0
- package/scripts/demo-studio.mjs +58 -0
- package/scripts/moondog +52 -0
- package/scripts/moondog.mjs +954 -0
- package/src/core/apple-data-migration.mjs +50 -0
- package/src/core/apple-library-source-status.mjs +220 -0
- package/src/core/apple-projection-domain-services.mjs +2356 -0
- package/src/core/capability-catalog.mjs +577 -0
- package/src/core/listening-profile-domain-services.mjs +1010 -0
- package/src/core/lyric-library.mjs +121 -0
- package/src/core/lyric-profile.mjs +50 -0
- package/src/core/lyric-service.mjs +41 -0
- package/src/core/moondog-application.mjs +1827 -0
- package/src/core/state-directory.mjs +19 -0
- package/src/core/synthetic-domain-services.mjs +1485 -0
- package/src/core/uuid-v5.mjs +42 -0
- package/src/demo/fictional-spotify-history.mjs +400 -0
- package/src/demo/moondog-demo.mjs +355 -0
- package/src/demo/moondog-interactive-tasteprint-demo.mjs +293 -0
- package/src/demo/moondog-tasteprint-demo.mjs +631 -0
- package/src/importers/apple-music-library/import-batch.mjs +290 -0
- package/src/importers/apple-music-library/index.mjs +76 -0
- package/src/importers/apple-music-library/normalize.mjs +1126 -0
- package/src/importers/apple-music-library/parse-plist.mjs +357 -0
- package/src/importers/apple-music-library/projection-records.mjs +557 -0
- package/src/importers/apple-music-library/read-batch.mjs +276 -0
- package/src/importers/apple-music-library/sqlite-projection.mjs +1307 -0
- package/src/importers/apple-music-library/stable-ids.mjs +19 -0
- package/src/importers/apple-music-library/track-snapshot.schema.json +136 -0
- package/src/integrations/apple-music/catalog.mjs +770 -0
- package/src/integrations/cross-catalog-artist-identity.mjs +222 -0
- package/src/integrations/listenbrainz/history-file.mjs +560 -0
- package/src/integrations/lyrics/lrclib.mjs +55 -0
- package/src/integrations/open-music-similarity/artist-radio.mjs +806 -0
- package/src/integrations/public-playlists.mjs +104 -0
- package/src/integrations/spotify/account-data-history.mjs +545 -0
- package/src/integrations/spotify/account-data-profile.mjs +918 -0
- package/src/integrations/spotify/authentication.mjs +791 -0
- package/src/integrations/spotify/catalog-resolver.mjs +657 -0
- package/src/integrations/spotify/connection.mjs +125 -0
- package/src/integrations/spotify/credential-store.mjs +26 -0
- package/src/integrations/spotify/extended-streaming-history.mjs +525 -0
- package/src/integrations/spotify/history-archive.mjs +59 -0
- package/src/integrations/spotify/index.mjs +80 -0
- package/src/integrations/spotify/recent-activity.mjs +198 -0
- package/src/integrations/spotify/resolution-cache.mjs +260 -0
- package/src/integrations/spotify/service.mjs +713 -0
- package/src/integrations/spotify/settings.mjs +89 -0
- package/src/integrations/spotify/web-api-client.mjs +761 -0
- package/src/integrations/web/codex-web.mjs +263 -0
- package/src/integrations/youtube-music/takeout.mjs +143 -0
- package/src/memory/local-memory-store.mjs +1705 -0
- package/src/memory/reflection-worker.mjs +87 -0
- package/src/profile/apple-library-import.mjs +28 -0
- package/src/profile/history-import.mjs +163 -0
- package/src/profile/listener-corrections.mjs +217 -0
- package/src/profile/listening-history-store.mjs +2055 -0
- package/src/profile/listening-profile-projection.mjs +2520 -0
- package/src/profile/local-music-data.mjs +812 -0
- package/src/profile/music-import-bundle.mjs +91 -0
- package/src/profile/music-providers.mjs +25 -0
- package/src/profile/spotify-archive-catalog-hints.mjs +210 -0
- package/src/profile/spotify-archive-taste.mjs +482 -0
- package/src/runtime/pi/agent-runtime.mjs +6444 -0
- package/src/runtime/pi/authentication.mjs +104 -0
- package/src/runtime/pi/configured-runtime.mjs +98 -0
- package/src/runtime/pi/memory-agent-runtime.mjs +337 -0
- package/src/runtime/pi/model-catalog.mjs +62 -0
- package/src/runtime/pi/model-transport.mjs +117 -0
- package/src/runtime/pi/persistent-credential-store.mjs +670 -0
- package/src/runtime/pi/provider-registry.mjs +74 -0
- package/src/runtime/pi/runtime-settings.mjs +81 -0
- package/src/surfaces/cli/auth-command.mjs +355 -0
- package/src/surfaces/cli/brand-components.mjs +390 -0
- package/src/surfaces/cli/brand-theme.mjs +231 -0
- package/src/surfaces/cli/catalog-command.mjs +509 -0
- package/src/surfaces/cli/command-completions.mjs +113 -0
- package/src/surfaces/cli/data-command.mjs +214 -0
- package/src/surfaces/cli/format-output.mjs +750 -0
- package/src/surfaces/cli/history-import-view.mjs +445 -0
- package/src/surfaces/cli/import-guides.mjs +9 -0
- package/src/surfaces/cli/listenbrainz-command.mjs +129 -0
- package/src/surfaces/cli/listening-menu.mjs +171 -0
- package/src/surfaces/cli/profile-command.mjs +290 -0
- package/src/surfaces/cli/spotify-command.mjs +849 -0
- package/src/surfaces/cli/taste-profile-model.mjs +306 -0
- package/src/surfaces/cli/taste-profile-view.mjs +272 -0
- package/src/surfaces/cli/terminal-art.mjs +295 -0
- package/src/surfaces/cli/tui.mjs +1714 -0
- package/src/surfaces/cli/web-command.mjs +52 -0
- package/src/surfaces/html/tasteprint-artifact.mjs +96 -0
- package/src/surfaces/html/tasteprint.mjs +3575 -0
- package/src/surfaces/web/showcase.mjs +272 -0
- package/src/surfaces/web/studio-page.mjs +4148 -0
- package/src/surfaces/web/studio.mjs +2288 -0
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
const providerLoaders = {
|
|
2
|
+
anthropic: async () => {
|
|
3
|
+
const { anthropicProvider } = await import(
|
|
4
|
+
"@earendil-works/pi-ai/providers/anthropic"
|
|
5
|
+
);
|
|
6
|
+
return anthropicProvider();
|
|
7
|
+
},
|
|
8
|
+
deepseek: async () => {
|
|
9
|
+
const { deepseekProvider } = await import(
|
|
10
|
+
"@earendil-works/pi-ai/providers/deepseek"
|
|
11
|
+
);
|
|
12
|
+
return deepseekProvider();
|
|
13
|
+
},
|
|
14
|
+
google: async () => {
|
|
15
|
+
const { googleProvider } = await import(
|
|
16
|
+
"@earendil-works/pi-ai/providers/google"
|
|
17
|
+
);
|
|
18
|
+
return googleProvider();
|
|
19
|
+
},
|
|
20
|
+
moonshotai: async () => {
|
|
21
|
+
const { moonshotaiProvider } = await import(
|
|
22
|
+
"@earendil-works/pi-ai/providers/moonshotai"
|
|
23
|
+
);
|
|
24
|
+
return moonshotaiProvider();
|
|
25
|
+
},
|
|
26
|
+
"moonshotai-cn": async () => {
|
|
27
|
+
const { moonshotaiCnProvider } = await import(
|
|
28
|
+
"@earendil-works/pi-ai/providers/moonshotai-cn"
|
|
29
|
+
);
|
|
30
|
+
return moonshotaiCnProvider();
|
|
31
|
+
},
|
|
32
|
+
openai: async () => {
|
|
33
|
+
const { openaiProvider } = await import(
|
|
34
|
+
"@earendil-works/pi-ai/providers/openai"
|
|
35
|
+
);
|
|
36
|
+
return openaiProvider();
|
|
37
|
+
},
|
|
38
|
+
"openai-codex": async () => {
|
|
39
|
+
const { openaiCodexProvider } = await import(
|
|
40
|
+
"@earendil-works/pi-ai/providers/openai-codex"
|
|
41
|
+
);
|
|
42
|
+
return openaiCodexProvider();
|
|
43
|
+
},
|
|
44
|
+
openrouter: async () => {
|
|
45
|
+
const { openrouterProvider } = await import(
|
|
46
|
+
"@earendil-works/pi-ai/providers/openrouter"
|
|
47
|
+
);
|
|
48
|
+
return openrouterProvider();
|
|
49
|
+
},
|
|
50
|
+
xai: async () => {
|
|
51
|
+
const { xaiProvider } = await import(
|
|
52
|
+
"@earendil-works/pi-ai/providers/xai"
|
|
53
|
+
);
|
|
54
|
+
return xaiProvider();
|
|
55
|
+
},
|
|
56
|
+
zai: async () => {
|
|
57
|
+
const { zaiApiProvider } = await import("./model-catalog.mjs");
|
|
58
|
+
return zaiApiProvider();
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
export const supportedPiProviderIds = Object.freeze(
|
|
63
|
+
Object.keys(providerLoaders),
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
export const apiKeyPiProviderIds = Object.freeze(
|
|
67
|
+
supportedPiProviderIds.filter((providerId) => providerId !== "openai-codex"),
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
export async function loadPiProvider(providerId) {
|
|
71
|
+
if (!Object.hasOwn(providerLoaders, providerId)) return undefined;
|
|
72
|
+
const loadProvider = providerLoaders[providerId];
|
|
73
|
+
return loadProvider();
|
|
74
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
import { resolveMoondogAuthFile } from "./persistent-credential-store.mjs";
|
|
5
|
+
|
|
6
|
+
const SETTINGS_VERSION = 1;
|
|
7
|
+
|
|
8
|
+
function settingsError(message, cause) {
|
|
9
|
+
const error = new Error(message, cause ? { cause } : undefined);
|
|
10
|
+
error.name = "MoondogRuntimeSettingsError";
|
|
11
|
+
error.code = "runtime_settings_invalid";
|
|
12
|
+
return error;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function normalizeSelection(value) {
|
|
16
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
17
|
+
throw settingsError("Moondog runtime settings must be an object.");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const provider =
|
|
21
|
+
typeof value.provider === "string"
|
|
22
|
+
? value.provider.trim().toLowerCase()
|
|
23
|
+
: "";
|
|
24
|
+
const model = typeof value.model === "string" ? value.model.trim() : "";
|
|
25
|
+
if (!provider || !model) {
|
|
26
|
+
throw settingsError(
|
|
27
|
+
"Moondog runtime settings require a provider and model.",
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
return { provider, model };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function resolveMoondogSettingsFile(environment = process.env) {
|
|
34
|
+
return path.join(
|
|
35
|
+
path.dirname(resolveMoondogAuthFile(environment)),
|
|
36
|
+
"settings.json",
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export async function readPiRuntimeSelection(environment = process.env) {
|
|
41
|
+
const settingsFile = resolveMoondogSettingsFile(environment);
|
|
42
|
+
let source;
|
|
43
|
+
try {
|
|
44
|
+
source = await readFile(settingsFile, "utf8");
|
|
45
|
+
} catch (error) {
|
|
46
|
+
if (error?.code === "ENOENT") return undefined;
|
|
47
|
+
throw settingsError("Moondog could not read runtime settings.", error);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
let document;
|
|
51
|
+
try {
|
|
52
|
+
document = JSON.parse(source);
|
|
53
|
+
} catch (error) {
|
|
54
|
+
throw settingsError("Moondog runtime settings are not valid JSON.", error);
|
|
55
|
+
}
|
|
56
|
+
if (
|
|
57
|
+
!document ||
|
|
58
|
+
typeof document !== "object" ||
|
|
59
|
+
Array.isArray(document) ||
|
|
60
|
+
document.version !== SETTINGS_VERSION ||
|
|
61
|
+
Object.keys(document).sort().join(",") !== "model,provider,version"
|
|
62
|
+
) {
|
|
63
|
+
throw settingsError("Moondog runtime settings use an unsupported shape.");
|
|
64
|
+
}
|
|
65
|
+
return normalizeSelection(document);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export async function writePiRuntimeSelection(
|
|
69
|
+
selection,
|
|
70
|
+
environment = process.env,
|
|
71
|
+
) {
|
|
72
|
+
const normalized = normalizeSelection(selection);
|
|
73
|
+
const settingsFile = resolveMoondogSettingsFile(environment);
|
|
74
|
+
await mkdir(path.dirname(settingsFile), { recursive: true, mode: 0o700 });
|
|
75
|
+
await writeFile(
|
|
76
|
+
settingsFile,
|
|
77
|
+
`${JSON.stringify({ version: SETTINGS_VERSION, ...normalized }, null, 2)}\n`,
|
|
78
|
+
{ encoding: "utf8", mode: 0o600 },
|
|
79
|
+
);
|
|
80
|
+
return normalized;
|
|
81
|
+
}
|
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
import { createInterface } from "node:readline/promises";
|
|
2
|
+
import { Writable } from "node:stream";
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
createPiAuthentication,
|
|
6
|
+
supportedAuthProviderIds,
|
|
7
|
+
} from "../../runtime/pi/authentication.mjs";
|
|
8
|
+
import { sanitizeTerminalText } from "./format-output.mjs";
|
|
9
|
+
|
|
10
|
+
const DEFAULT_AUTH_PROVIDER = "openai-codex";
|
|
11
|
+
|
|
12
|
+
export function authHelpText() {
|
|
13
|
+
return `# Moondog authentication
|
|
14
|
+
|
|
15
|
+
Commands:
|
|
16
|
+
|
|
17
|
+
- \`moondog auth login openai-codex\` - start a browser OAuth login
|
|
18
|
+
- \`moondog auth login openai-codex --device-code\` - use headless device login
|
|
19
|
+
- \`moondog auth login <provider>\` - save an API key using hidden terminal input
|
|
20
|
+
- \`moondog auth status [provider] [--json]\` - inspect saved or environment credentials
|
|
21
|
+
- \`moondog auth logout <provider> [--json]\` - remove that provider's saved credential
|
|
22
|
+
|
|
23
|
+
API key providers: ${supportedAuthProviderIds.filter((id) => id !== DEFAULT_AUTH_PROVIDER).join(", ")}.
|
|
24
|
+
Use zai for GLM, moonshotai or moonshotai-cn for Kimi, xai for Grok, openai for GPT, and anthropic for Claude.
|
|
25
|
+
Moondog keeps credentials in its own private local store.
|
|
26
|
+
API keys are entered interactively, never as command arguments.
|
|
27
|
+
It does not read, modify, or log out the Codex CLI or ChatGPT desktop session.`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function parseAuthArguments(args) {
|
|
31
|
+
const values = [...args];
|
|
32
|
+
if (
|
|
33
|
+
values.length === 1 &&
|
|
34
|
+
(values[0] === "--help" || values[0] === "-h")
|
|
35
|
+
) {
|
|
36
|
+
return {
|
|
37
|
+
action: "help",
|
|
38
|
+
provider: undefined,
|
|
39
|
+
extra: [],
|
|
40
|
+
loginMethod: "browser",
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
const deviceCodeIndex = values.indexOf("--device-code");
|
|
44
|
+
const browserIndex = values.indexOf("--browser");
|
|
45
|
+
const deviceCode = deviceCodeIndex !== -1;
|
|
46
|
+
const browser = browserIndex !== -1;
|
|
47
|
+
if (deviceCode) values.splice(deviceCodeIndex, 1);
|
|
48
|
+
const adjustedBrowserIndex = values.indexOf("--browser");
|
|
49
|
+
if (adjustedBrowserIndex !== -1) values.splice(adjustedBrowserIndex, 1);
|
|
50
|
+
if (deviceCode && browser) {
|
|
51
|
+
throw new Error("Choose either --browser or --device-code, not both.");
|
|
52
|
+
}
|
|
53
|
+
const unknownOption = values.find((value) => value.startsWith("-"));
|
|
54
|
+
if (unknownOption) {
|
|
55
|
+
throw new Error(
|
|
56
|
+
"Unknown auth option. API keys must be entered at the hidden prompt, never as command arguments.",
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
return {
|
|
60
|
+
action: values[0],
|
|
61
|
+
provider: values[1],
|
|
62
|
+
extra: values.slice(2),
|
|
63
|
+
loginMethod: deviceCode ? "device_code" : "browser",
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function writeLine(stream, value = "") {
|
|
68
|
+
stream.write(`${sanitizeTerminalText(value)}\n`);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function formatStatus(value) {
|
|
72
|
+
if (value.state === "environment") {
|
|
73
|
+
return `${value.provider}: configured through ${value.source ?? "environment"}`;
|
|
74
|
+
}
|
|
75
|
+
if (value.state === "stored") {
|
|
76
|
+
return `${value.provider}: stored ${value.type} credential`;
|
|
77
|
+
}
|
|
78
|
+
return `${value.provider}: not configured`;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function createReadlineOutput(output) {
|
|
82
|
+
let muted = false;
|
|
83
|
+
const stream = new Writable({
|
|
84
|
+
write(chunk, encoding, callback) {
|
|
85
|
+
if (!muted) output.write(chunk, encoding);
|
|
86
|
+
callback();
|
|
87
|
+
},
|
|
88
|
+
});
|
|
89
|
+
stream.isTTY = output.isTTY;
|
|
90
|
+
stream.columns = output.columns;
|
|
91
|
+
stream.rows = output.rows;
|
|
92
|
+
if (typeof output.getColorDepth === "function") {
|
|
93
|
+
stream.getColorDepth = output.getColorDepth.bind(output);
|
|
94
|
+
}
|
|
95
|
+
return {
|
|
96
|
+
stream,
|
|
97
|
+
setMuted(value) {
|
|
98
|
+
muted = value;
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function createAuthInteraction({
|
|
104
|
+
input,
|
|
105
|
+
output,
|
|
106
|
+
loginMethod,
|
|
107
|
+
signal,
|
|
108
|
+
interactive,
|
|
109
|
+
apiKeyLogin,
|
|
110
|
+
}) {
|
|
111
|
+
const readlineOutput = createReadlineOutput(output);
|
|
112
|
+
readlineOutput.setMuted(apiKeyLogin);
|
|
113
|
+
const readline = createInterface({
|
|
114
|
+
input,
|
|
115
|
+
output: readlineOutput.stream,
|
|
116
|
+
terminal: interactive,
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
close() {
|
|
121
|
+
readline.close();
|
|
122
|
+
},
|
|
123
|
+
interaction: {
|
|
124
|
+
signal,
|
|
125
|
+
async prompt(prompt) {
|
|
126
|
+
if (prompt.type === "select") {
|
|
127
|
+
const selected = prompt.options.find(
|
|
128
|
+
(option) => option.id === loginMethod,
|
|
129
|
+
);
|
|
130
|
+
if (!selected) {
|
|
131
|
+
throw new Error(
|
|
132
|
+
"The installed Pi provider does not support the requested login method.",
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
return selected.id;
|
|
136
|
+
}
|
|
137
|
+
if (prompt.type === "secret" && !interactive) {
|
|
138
|
+
throw new Error(
|
|
139
|
+
"API key login requires an interactive terminal with hidden input.",
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
const placeholder = prompt.placeholder
|
|
143
|
+
? ` (${sanitizeTerminalText(prompt.placeholder)})`
|
|
144
|
+
: "";
|
|
145
|
+
const question = `${sanitizeTerminalText(prompt.message)}${placeholder}: `;
|
|
146
|
+
const questionOptions = { signal: prompt.signal ?? signal };
|
|
147
|
+
if (prompt.type !== "manual_code" && prompt.type !== "secret") {
|
|
148
|
+
return readline.question(question, questionOptions);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
writeLine(output);
|
|
152
|
+
writeLine(output, question);
|
|
153
|
+
writeLine(
|
|
154
|
+
output,
|
|
155
|
+
prompt.type === "secret"
|
|
156
|
+
? "Paste the API key. Input is hidden."
|
|
157
|
+
: "Paste the authorization response. Input is hidden for safety.",
|
|
158
|
+
);
|
|
159
|
+
readlineOutput.setMuted(true);
|
|
160
|
+
try {
|
|
161
|
+
const answer = await readline.question("", questionOptions);
|
|
162
|
+
return prompt.type === "secret" ? answer.trim() : answer;
|
|
163
|
+
} finally {
|
|
164
|
+
readlineOutput.setMuted(apiKeyLogin);
|
|
165
|
+
writeLine(output);
|
|
166
|
+
}
|
|
167
|
+
},
|
|
168
|
+
notify(event) {
|
|
169
|
+
switch (event.type) {
|
|
170
|
+
case "auth_url":
|
|
171
|
+
writeLine(output);
|
|
172
|
+
writeLine(output, "Open this URL to sign in with ChatGPT:");
|
|
173
|
+
writeLine(output, event.url);
|
|
174
|
+
if (event.instructions) writeLine(output, event.instructions);
|
|
175
|
+
break;
|
|
176
|
+
case "device_code":
|
|
177
|
+
writeLine(output);
|
|
178
|
+
writeLine(output, "Open this URL to sign in with ChatGPT:");
|
|
179
|
+
writeLine(output, event.verificationUri);
|
|
180
|
+
writeLine(output, `Device code: ${event.userCode}`);
|
|
181
|
+
break;
|
|
182
|
+
case "info":
|
|
183
|
+
writeLine(output, event.message);
|
|
184
|
+
for (const link of event.links ?? []) {
|
|
185
|
+
writeLine(
|
|
186
|
+
output,
|
|
187
|
+
`${link.label ?? "More information"}: ${link.url}`,
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
break;
|
|
191
|
+
case "progress":
|
|
192
|
+
writeLine(output, event.message);
|
|
193
|
+
break;
|
|
194
|
+
}
|
|
195
|
+
},
|
|
196
|
+
},
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function wrapLoginError(error, provider) {
|
|
201
|
+
const name = provider === DEFAULT_AUTH_PROVIDER ? "OpenAI Codex" : provider;
|
|
202
|
+
if (error?.name === "AbortError" || error?.code === "ABORT_ERR") {
|
|
203
|
+
return new Error(`${name} login was cancelled.`);
|
|
204
|
+
}
|
|
205
|
+
if (
|
|
206
|
+
typeof error?.code === "string" &&
|
|
207
|
+
(error.code.startsWith("credential_store_") ||
|
|
208
|
+
error.code === "credential_provider_unsupported")
|
|
209
|
+
) {
|
|
210
|
+
return error;
|
|
211
|
+
}
|
|
212
|
+
const safe = new Error(
|
|
213
|
+
`${name} login failed before a credential was saved.`,
|
|
214
|
+
);
|
|
215
|
+
safe.code = "auth_login_failed";
|
|
216
|
+
return safe;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export async function runAuthCommand({
|
|
220
|
+
args,
|
|
221
|
+
json = false,
|
|
222
|
+
environment = process.env,
|
|
223
|
+
input = process.stdin,
|
|
224
|
+
output = process.stdout,
|
|
225
|
+
progressOutput = process.stderr,
|
|
226
|
+
signalTarget = process,
|
|
227
|
+
createAuthentication = createPiAuthentication,
|
|
228
|
+
interactive = Boolean(input.isTTY && progressOutput.isTTY),
|
|
229
|
+
}) {
|
|
230
|
+
const options = parseAuthArguments(args);
|
|
231
|
+
if (!options.action || options.action === "help") {
|
|
232
|
+
if (options.provider || options.extra.length > 0) {
|
|
233
|
+
throw new Error("The auth help command does not accept extra arguments.");
|
|
234
|
+
}
|
|
235
|
+
writeLine(output, authHelpText());
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (options.extra.length > 0) {
|
|
240
|
+
throw new Error("Too many arguments for the auth command.");
|
|
241
|
+
}
|
|
242
|
+
if (!["login", "status", "logout"].includes(options.action)) {
|
|
243
|
+
throw new Error("Unknown auth command. Use login, status, logout, or help.");
|
|
244
|
+
}
|
|
245
|
+
if (
|
|
246
|
+
options.action !== "login" &&
|
|
247
|
+
(options.loginMethod !== "browser" || args.includes("--browser"))
|
|
248
|
+
) {
|
|
249
|
+
throw new Error("Login method options are valid only for auth login.");
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const provider = options.provider ??
|
|
253
|
+
(options.action === "status" ? DEFAULT_AUTH_PROVIDER : undefined);
|
|
254
|
+
if (!provider) {
|
|
255
|
+
throw new Error(`The auth ${options.action} command requires a provider.`);
|
|
256
|
+
}
|
|
257
|
+
if (!supportedAuthProviderIds.includes(provider)) {
|
|
258
|
+
throw new Error(
|
|
259
|
+
`Unsupported auth provider. Choose ${supportedAuthProviderIds.join(", ")}.`,
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
const apiKeyLogin = provider !== DEFAULT_AUTH_PROVIDER;
|
|
263
|
+
if (
|
|
264
|
+
apiKeyLogin &&
|
|
265
|
+
(args.includes("--device-code") || args.includes("--browser"))
|
|
266
|
+
) {
|
|
267
|
+
throw new Error(
|
|
268
|
+
"Browser and device-code options apply only to openai-codex OAuth login.",
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if (options.action === "status") {
|
|
273
|
+
const authentication = await createAuthentication({ environment });
|
|
274
|
+
const status = await authentication.status(provider);
|
|
275
|
+
if (json) {
|
|
276
|
+
writeLine(output, JSON.stringify(status, null, 2));
|
|
277
|
+
} else {
|
|
278
|
+
writeLine(output, formatStatus(status));
|
|
279
|
+
writeLine(
|
|
280
|
+
output,
|
|
281
|
+
"Status reports local configuration only. It does not verify the credential with the provider or refresh tokens.",
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
if (options.action === "logout") {
|
|
288
|
+
const authentication = await createAuthentication({ environment });
|
|
289
|
+
const status = await authentication.logout(provider);
|
|
290
|
+
if (json) {
|
|
291
|
+
writeLine(output, JSON.stringify(status, null, 2));
|
|
292
|
+
} else {
|
|
293
|
+
writeLine(output, formatStatus(status));
|
|
294
|
+
writeLine(
|
|
295
|
+
output,
|
|
296
|
+
apiKeyLogin
|
|
297
|
+
? "Moondog's saved API key was removed. Environment credentials remain available until unset."
|
|
298
|
+
: "Moondog's local credential was removed. Codex CLI and ChatGPT desktop login were not changed.",
|
|
299
|
+
);
|
|
300
|
+
}
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
if (json) {
|
|
305
|
+
throw new Error("The auth login command does not support --json.");
|
|
306
|
+
}
|
|
307
|
+
if (apiKeyLogin && !interactive) {
|
|
308
|
+
throw new Error(
|
|
309
|
+
"API key login requires an interactive terminal with hidden input. For headless use, set the provider's API key environment variable.",
|
|
310
|
+
);
|
|
311
|
+
}
|
|
312
|
+
if (!apiKeyLogin && options.loginMethod === "browser" && !interactive) {
|
|
313
|
+
throw new Error(
|
|
314
|
+
"Browser OAuth login requires an interactive terminal. Use --device-code for a headless session.",
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const authentication = await createAuthentication({ environment });
|
|
319
|
+
const controller = new AbortController();
|
|
320
|
+
const handleSigint = () => controller.abort();
|
|
321
|
+
signalTarget.once("SIGINT", handleSigint);
|
|
322
|
+
const login = createAuthInteraction({
|
|
323
|
+
input,
|
|
324
|
+
output: progressOutput,
|
|
325
|
+
loginMethod: options.loginMethod,
|
|
326
|
+
signal: controller.signal,
|
|
327
|
+
interactive,
|
|
328
|
+
apiKeyLogin,
|
|
329
|
+
});
|
|
330
|
+
try {
|
|
331
|
+
let status;
|
|
332
|
+
try {
|
|
333
|
+
status = await authentication.login(provider, login.interaction);
|
|
334
|
+
} catch (error) {
|
|
335
|
+
throw wrapLoginError(error, provider);
|
|
336
|
+
}
|
|
337
|
+
writeLine(output, formatStatus(status));
|
|
338
|
+
if (apiKeyLogin) {
|
|
339
|
+
writeLine(
|
|
340
|
+
output,
|
|
341
|
+
"API key saved locally. It will be checked by the provider when you send a request.",
|
|
342
|
+
);
|
|
343
|
+
}
|
|
344
|
+
writeLine(output, "Next, return to Moondog and run /model.");
|
|
345
|
+
if (!apiKeyLogin) {
|
|
346
|
+
writeLine(output, "For a non-interactive launch, use environment overrides:");
|
|
347
|
+
writeLine(output, " export MOONDOG_PROVIDER=openai-codex");
|
|
348
|
+
writeLine(output, " export MOONDOG_MODEL=gpt-5.6-terra");
|
|
349
|
+
writeLine(output, " moondog");
|
|
350
|
+
}
|
|
351
|
+
} finally {
|
|
352
|
+
signalTarget.removeListener("SIGINT", handleSigint);
|
|
353
|
+
login.close();
|
|
354
|
+
}
|
|
355
|
+
}
|