@duckmind/dm-windows-x64 0.60.6 → 0.60.8
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/dm.exe +0 -0
- package/extensions/.dm-extensions.json +70 -123
- package/extensions/dm-9router-ext/src/index.js +410 -5
- package/extensions/dm-caveman/extensions/caveman.js +283 -12
- package/extensions/dm-cliproxy/index.js +182 -2
- package/extensions/dm-cliproxy/scripts/check-config-migration.js +66 -8
- package/extensions/dm-cliproxy/src/apply.js +228 -1
- package/extensions/dm-cliproxy/src/cache.js +42 -1
- package/extensions/dm-cliproxy/src/commands.js +50 -2
- package/extensions/dm-cliproxy/src/compat.js +81 -1
- package/extensions/dm-cliproxy/src/config.js +192 -2
- package/extensions/dm-cliproxy/src/conflicts.js +46 -1
- package/extensions/dm-cliproxy/src/fetch-models.js +190 -1
- package/extensions/dm-cliproxy/src/fetch-usage.js +41 -1
- package/extensions/dm-cliproxy/src/log.js +23 -1
- package/extensions/dm-cliproxy/src/status-quota.js +77 -1
- package/extensions/dm-cliproxy/src/ui-frame.js +50 -1
- package/extensions/dm-cliproxy/src/ui-hub/hub.js +199 -2
- package/extensions/dm-cliproxy/src/ui-hub/index.js +26 -2
- package/extensions/dm-cliproxy/src/ui-hub/shell.js +46 -1
- package/extensions/dm-cliproxy/src/ui-hub/view-diagnostics.js +69 -1
- package/extensions/dm-cliproxy/src/ui-hub/view-models.js +379 -1
- package/extensions/dm-cliproxy/src/ui-hub/view-usage.js +106 -1
- package/extensions/dm-cliproxy/src/ui-picker/catalog.js +41 -1
- package/extensions/dm-cliproxy/src/ui-picker/mutate.js +115 -1
- package/extensions/dm-cliproxy/src/ui-picker/prompt-confirm.js +35 -1
- package/extensions/dm-cliproxy/src/ui-picker/prompt-name.js +62 -1
- package/extensions/dm-cliproxy/src/ui-picker/providers.js +45 -1
- package/extensions/dm-cliproxy/src/ui-picker/render-text.js +34 -1
- package/extensions/dm-cliproxy/src/ui-picker/rows.js +57 -1
- package/extensions/dm-cliproxy/src/ui-setup.js +260 -2
- package/extensions/dm-cliproxy/src/ui-usage.js +151 -1
- package/extensions/dm-cliproxy/src/usage-shared-cache.js +97 -1
- package/extensions/dm-context/src/context.js +144 -1
- package/extensions/dm-context/src/index.js +339 -7
- package/extensions/dm-context/src/utils.js +9 -1
- package/extensions/dm-cua/bin/browser-cua.mjs +73 -8
- package/extensions/dm-cua/index.js +75 -6
- package/extensions/dm-cua/src/browser-cua-lib.mjs +490 -6
- package/extensions/dm-cua/src/browser-install.mjs +331 -2
- package/extensions/dm-fff/src/index.js +688 -12
- package/extensions/dm-fff/src/query.js +60 -1
- package/extensions/dm-goal/src/goal.js +894 -23
- package/extensions/dm-image2/index.js +103 -9
- package/extensions/dm-image2/src/image-lib.mjs +1275 -8
- package/extensions/dm-subagents/install.mjs +62 -8
- package/extensions/dm-subagents/src/agents/agent-management.js +1190 -36
- package/extensions/dm-subagents/src/agents/agent-memory.js +216 -6
- package/extensions/dm-subagents/src/agents/agent-scope.js +5 -1
- package/extensions/dm-subagents/src/agents/agent-selection.js +20 -1
- package/extensions/dm-subagents/src/agents/agent-serializer.js +120 -5
- package/extensions/dm-subagents/src/agents/agents.js +1139 -11
- package/extensions/dm-subagents/src/agents/chain-serializer.js +299 -11
- package/extensions/dm-subagents/src/agents/frontmatter.js +65 -6
- package/extensions/dm-subagents/src/agents/identity.js +29 -1
- package/extensions/dm-subagents/src/agents/proactive-skills.js +141 -1
- package/extensions/dm-subagents/src/agents/skills.js +614 -6
- package/extensions/dm-subagents/src/extension/config.js +35 -2
- package/extensions/dm-subagents/src/extension/control-notices.js +69 -4
- package/extensions/dm-subagents/src/extension/doctor.js +172 -15
- package/extensions/dm-subagents/src/extension/fanout-child.js +158 -231
- package/extensions/dm-subagents/src/extension/index.js +521 -359
- package/extensions/dm-subagents/src/extension/rpc.js +266 -7
- package/extensions/dm-subagents/src/extension/schemas.js +275 -1
- package/extensions/dm-subagents/src/extension/tool-description.js +111 -6
- package/extensions/dm-subagents/src/intercom/intercom-bridge.js +126 -4
- package/extensions/dm-subagents/src/intercom/native-supervisor-channel.js +452 -5
- package/extensions/dm-subagents/src/intercom/result-intercom.js +319 -3
- package/extensions/dm-subagents/src/profiles/profiles.js +458 -3
- package/extensions/dm-subagents/src/runs/background/async-execution.js +834 -40
- package/extensions/dm-subagents/src/runs/background/async-job-tracker.js +435 -14
- package/extensions/dm-subagents/src/runs/background/async-resume.js +334 -8
- package/extensions/dm-subagents/src/runs/background/async-status.js +313 -12
- package/extensions/dm-subagents/src/runs/background/chain-append.js +245 -2
- package/extensions/dm-subagents/src/runs/background/chain-root-attachment.js +136 -1
- package/extensions/dm-subagents/src/runs/background/completion-batcher.js +94 -1
- package/extensions/dm-subagents/src/runs/background/completion-dedupe.js +54 -1
- package/extensions/dm-subagents/src/runs/background/control-channel.js +190 -1
- package/extensions/dm-subagents/src/runs/background/fleet-view.js +483 -17
- package/extensions/dm-subagents/src/runs/background/notify.js +129 -3
- package/extensions/dm-subagents/src/runs/background/parallel-groups.js +34 -1
- package/extensions/dm-subagents/src/runs/background/result-watcher.js +236 -6
- package/extensions/dm-subagents/src/runs/background/run-id-resolver.js +76 -4
- package/extensions/dm-subagents/src/runs/background/run-status.js +427 -23
- package/extensions/dm-subagents/src/runs/background/scheduled-runs.js +487 -4
- package/extensions/dm-subagents/src/runs/background/stale-run-reconciler.js +306 -9
- package/extensions/dm-subagents/src/runs/background/subagent-runner.js +2849 -73
- package/extensions/dm-subagents/src/runs/background/top-level-async.js +5 -1
- package/extensions/dm-subagents/src/runs/background/wait.js +206 -11
- package/extensions/dm-subagents/src/runs/foreground/chain-clarify.js +1013 -12
- package/extensions/dm-subagents/src/runs/foreground/chain-execution.js +980 -101
- package/extensions/dm-subagents/src/runs/foreground/execution.js +1165 -45
- package/extensions/dm-subagents/src/runs/foreground/subagent-executor.js +3157 -222
- package/extensions/dm-subagents/src/runs/shared/acceptance.js +835 -3
- package/extensions/dm-subagents/src/runs/shared/chain-outputs.js +104 -1
- package/extensions/dm-subagents/src/runs/shared/completion-guard.js +116 -3
- package/extensions/dm-subagents/src/runs/shared/dm-args.js +208 -1
- package/extensions/dm-subagents/src/runs/shared/dm-spawn.js +90 -1
- package/extensions/dm-subagents/src/runs/shared/dynamic-fanout.js +282 -1
- package/extensions/dm-subagents/src/runs/shared/long-running-guard.js +148 -1
- package/extensions/dm-subagents/src/runs/shared/mcp-direct-tool-allowlist.js +305 -1
- package/extensions/dm-subagents/src/runs/shared/model-fallback.js +194 -1
- package/extensions/dm-subagents/src/runs/shared/model-scope.js +65 -1
- package/extensions/dm-subagents/src/runs/shared/nested-events.js +851 -8
- package/extensions/dm-subagents/src/runs/shared/nested-path.js +41 -1
- package/extensions/dm-subagents/src/runs/shared/nested-render.js +105 -1
- package/extensions/dm-subagents/src/runs/shared/parallel-utils.js +81 -4
- package/extensions/dm-subagents/src/runs/shared/run-history.js +51 -4
- package/extensions/dm-subagents/src/runs/shared/single-output.js +149 -8
- package/extensions/dm-subagents/src/runs/shared/structured-output.js +58 -1
- package/extensions/dm-subagents/src/runs/shared/subagent-control.js +166 -5
- package/extensions/dm-subagents/src/runs/shared/subagent-prompt-runtime.js +329 -13
- package/extensions/dm-subagents/src/runs/shared/tool-budget.js +73 -1
- package/extensions/dm-subagents/src/runs/shared/turn-budget.js +47 -4
- package/extensions/dm-subagents/src/runs/shared/workflow-graph.js +196 -1
- package/extensions/dm-subagents/src/runs/shared/worktree.js +435 -3
- package/extensions/dm-subagents/src/shared/artifacts.js +92 -2
- package/extensions/dm-subagents/src/shared/atomic-json.js +55 -1
- package/extensions/dm-subagents/src/shared/child-transcript.js +167 -5
- package/extensions/dm-subagents/src/shared/file-coalescer.js +25 -1
- package/extensions/dm-subagents/src/shared/fork-context.js +147 -4
- package/extensions/dm-subagents/src/shared/formatters.js +98 -7
- package/extensions/dm-subagents/src/shared/jsonl-writer.js +56 -2
- package/extensions/dm-subagents/src/shared/model-info.js +62 -1
- package/extensions/dm-subagents/src/shared/post-exit-stdio-guard.js +68 -1
- package/extensions/dm-subagents/src/shared/session-identity.js +6 -1
- package/extensions/dm-subagents/src/shared/session-tokens.js +39 -2
- package/extensions/dm-subagents/src/shared/settings.js +198 -11
- package/extensions/dm-subagents/src/shared/status-format.js +53 -1
- package/extensions/dm-subagents/src/shared/types.js +184 -6
- package/extensions/dm-subagents/src/shared/utils.js +462 -2
- package/extensions/dm-subagents/src/slash/prompt-template-bridge.js +288 -1
- package/extensions/dm-subagents/src/slash/prompt-workflows.js +297 -7
- package/extensions/dm-subagents/src/slash/slash-bridge.js +118 -1
- package/extensions/dm-subagents/src/slash/slash-commands.js +1287 -31
- package/extensions/dm-subagents/src/slash/slash-live-state.js +240 -4
- package/extensions/dm-subagents/src/tui/render-helpers.js +64 -1
- package/extensions/dm-subagents/src/tui/render.js +1542 -4
- package/extensions/dm-usage/index.js +1294 -9
- package/extensions/greedysearch-dm/bin/cdp-greedy.mjs +40 -9
- package/extensions/greedysearch-dm/bin/cdp-headless.mjs +5 -2
- package/extensions/greedysearch-dm/bin/cdp-visible.mjs +5 -2
- package/extensions/greedysearch-dm/bin/cdp.mjs +896 -30
- package/extensions/greedysearch-dm/bin/gschrome.mjs +30 -2
- package/extensions/greedysearch-dm/bin/kill-visible.mjs +7 -2
- package/extensions/greedysearch-dm/bin/launch-visible.mjs +13 -2
- package/extensions/greedysearch-dm/bin/launch.mjs +282 -10
- package/extensions/greedysearch-dm/bin/mcp.mjs +386 -361
- package/extensions/greedysearch-dm/bin/search.mjs +620 -540
- package/extensions/greedysearch-dm/bin/visible.mjs +22 -2
- package/extensions/greedysearch-dm/extractors/bing-copilot.mjs +329 -579
- package/extensions/greedysearch-dm/extractors/chatgpt.mjs +301 -583
- package/extensions/greedysearch-dm/extractors/common.mjs +408 -32
- package/extensions/greedysearch-dm/extractors/consensus.mjs +376 -365
- package/extensions/greedysearch-dm/extractors/consent.mjs +303 -14
- package/extensions/greedysearch-dm/extractors/gemini.mjs +228 -592
- package/extensions/greedysearch-dm/extractors/google-ai.mjs +78 -499
- package/extensions/greedysearch-dm/extractors/logically.mjs +270 -347
- package/extensions/greedysearch-dm/extractors/perplexity.mjs +243 -581
- package/extensions/greedysearch-dm/extractors/selectors.mjs +32 -1
- package/extensions/greedysearch-dm/extractors/semantic-scholar.mjs +130 -317
- package/extensions/greedysearch-dm/index.js +123 -23
- package/extensions/greedysearch-dm/src/fetcher.mjs +576 -2
- package/extensions/greedysearch-dm/src/formatters/results.js +95 -10
- package/extensions/greedysearch-dm/src/formatters/sources.js +57 -1
- package/extensions/greedysearch-dm/src/formatters/synthesis.js +49 -1
- package/extensions/greedysearch-dm/src/github.mjs +222 -7
- package/extensions/greedysearch-dm/src/reddit.mjs +145 -14
- package/extensions/greedysearch-dm/src/search/browser-lifecycle.mjs +340 -9
- package/extensions/greedysearch-dm/src/search/challenge-detect.mjs +112 -4
- package/extensions/greedysearch-dm/src/search/chrome.mjs +486 -285
- package/extensions/greedysearch-dm/src/search/constants.mjs +109 -8
- package/extensions/greedysearch-dm/src/search/defaults.mjs +10 -1
- package/extensions/greedysearch-dm/src/search/engines.mjs +79 -9
- package/extensions/greedysearch-dm/src/search/fetch-source.mjs +441 -349
- package/extensions/greedysearch-dm/src/search/file-sources.mjs +29 -7
- package/extensions/greedysearch-dm/src/search/minimize.mjs +86 -1
- package/extensions/greedysearch-dm/src/search/output.mjs +51 -5
- package/extensions/greedysearch-dm/src/search/paths.mjs +48 -1
- package/extensions/greedysearch-dm/src/search/pdf.mjs +63 -2
- package/extensions/greedysearch-dm/src/search/port-pid.mjs +69 -1
- package/extensions/greedysearch-dm/src/search/progress.mjs +109 -2
- package/extensions/greedysearch-dm/src/search/query.mjs +21 -1
- package/extensions/greedysearch-dm/src/search/recovery.mjs +49 -1
- package/extensions/greedysearch-dm/src/search/research.mjs +2227 -458
- package/extensions/greedysearch-dm/src/search/scale-aware.mjs +61 -11
- package/extensions/greedysearch-dm/src/search/simple-research.mjs +396 -805
- package/extensions/greedysearch-dm/src/search/sources.mjs +412 -1
- package/extensions/greedysearch-dm/src/search/synthesis-runner.mjs +127 -12
- package/extensions/greedysearch-dm/src/search/synthesis.mjs +202 -12
- package/extensions/greedysearch-dm/src/tools/greedy-search-handler.js +209 -23
- package/extensions/greedysearch-dm/src/tools/shared.js +226 -10
- package/extensions/greedysearch-dm/src/utils/content.mjs +35 -4
- package/extensions/greedysearch-dm/src/utils/helpers.js +22 -1
- package/extensions/greedysearch-dm/src/utils/node-runtime.mjs +10 -1
- package/extensions/greedysearch-dm/src/utils/system-cmds.mjs +61 -1
- package/package.json +1 -1
|
@@ -1,2 +1,192 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
1
|
+
import { execSync } from "node:child_process";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import {
|
|
4
|
+
chmodSync,
|
|
5
|
+
closeSync,
|
|
6
|
+
constants,
|
|
7
|
+
copyFileSync,
|
|
8
|
+
existsSync,
|
|
9
|
+
fsyncSync,
|
|
10
|
+
linkSync,
|
|
11
|
+
mkdirSync,
|
|
12
|
+
openSync,
|
|
13
|
+
readFileSync,
|
|
14
|
+
renameSync,
|
|
15
|
+
rmSync,
|
|
16
|
+
writeFileSync
|
|
17
|
+
} from "node:fs";
|
|
18
|
+
import { homedir } from "node:os";
|
|
19
|
+
import { dirname, join } from "node:path";
|
|
20
|
+
import { fileURLToPath } from "node:url";
|
|
21
|
+
import { log } from "./log.js";
|
|
22
|
+
export const CONFIG_DIR = join(homedir(), ".dm", "agent", "dm-cliproxy");
|
|
23
|
+
export const CONFIG_PATH = join(CONFIG_DIR, "config.json");
|
|
24
|
+
export const LEGACY_CONFIG_PATHS = [
|
|
25
|
+
join(homedir(), ".pi", "agent", "pi-cliproxyapi", "config.json"),
|
|
26
|
+
join(homedir(), ".config", "pi-cliproxyapi", "config.json")
|
|
27
|
+
];
|
|
28
|
+
export function ensurePrivateDirectory(path) {
|
|
29
|
+
mkdirSync(path, { recursive: true, mode: 448 });
|
|
30
|
+
chmodSync(path, 448);
|
|
31
|
+
}
|
|
32
|
+
export function ensurePrivateFile(path) {
|
|
33
|
+
if (existsSync(path))
|
|
34
|
+
chmodSync(path, 384);
|
|
35
|
+
}
|
|
36
|
+
function hardenReferencedSecretFiles(configPath) {
|
|
37
|
+
let raw;
|
|
38
|
+
try {
|
|
39
|
+
raw = JSON.parse(readFileSync(configPath, "utf8"));
|
|
40
|
+
} catch {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
const proxy = raw && typeof raw === "object" ? raw.proxy : undefined;
|
|
44
|
+
for (const value of [proxy?.apiKey, proxy?.usageKey]) {
|
|
45
|
+
if (typeof value !== "string")
|
|
46
|
+
continue;
|
|
47
|
+
const match = /^!cat\s+((?:~\/|\/)[^\s;&|`]+)$/.exec(value.trim());
|
|
48
|
+
if (!match)
|
|
49
|
+
continue;
|
|
50
|
+
const secretPath = match[1].replace(/^~/, homedir());
|
|
51
|
+
ensurePrivateFile(secretPath);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
export function isLocalCheckout() {
|
|
55
|
+
return existsSync(join(dirname(fileURLToPath(import.meta.url)), "..", ".git"));
|
|
56
|
+
}
|
|
57
|
+
export function migrateLegacyConfig(legacyPaths = LEGACY_CONFIG_PATHS, configPath = CONFIG_PATH, _localCheckout = isLocalCheckout()) {
|
|
58
|
+
if (existsSync(configPath))
|
|
59
|
+
return;
|
|
60
|
+
const candidates = typeof legacyPaths === "string" ? [legacyPaths] : [...legacyPaths];
|
|
61
|
+
const legacyPath = candidates.find((candidate) => existsSync(candidate));
|
|
62
|
+
if (!legacyPath)
|
|
63
|
+
return;
|
|
64
|
+
let temporaryPath;
|
|
65
|
+
try {
|
|
66
|
+
ensurePrivateFile(legacyPath);
|
|
67
|
+
hardenReferencedSecretFiles(legacyPath);
|
|
68
|
+
ensurePrivateDirectory(dirname(configPath));
|
|
69
|
+
temporaryPath = `${configPath}.${process.pid}.${randomUUID()}.tmp`;
|
|
70
|
+
copyFileSync(legacyPath, temporaryPath, constants.COPYFILE_EXCL);
|
|
71
|
+
chmodSync(temporaryPath, 384);
|
|
72
|
+
linkSync(temporaryPath, configPath);
|
|
73
|
+
rmSync(temporaryPath);
|
|
74
|
+
temporaryPath = undefined;
|
|
75
|
+
log.info("legacy config copied to", configPath);
|
|
76
|
+
} catch (err) {
|
|
77
|
+
if (temporaryPath) {
|
|
78
|
+
try {
|
|
79
|
+
rmSync(temporaryPath, { force: true });
|
|
80
|
+
} catch (cleanupErr) {
|
|
81
|
+
log.warn("failed to clean temporary config:", cleanupErr);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
log.warn("failed to migrate legacy config:", err);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
const DEFAULT_CONFIG = {
|
|
88
|
+
proxy: {
|
|
89
|
+
endpoint: "",
|
|
90
|
+
apiKey: ""
|
|
91
|
+
},
|
|
92
|
+
builtinProviders: {},
|
|
93
|
+
customProviders: {},
|
|
94
|
+
discoveryExcludes: ["*:*"],
|
|
95
|
+
overrides: {},
|
|
96
|
+
refreshIntervalMinutes: 0,
|
|
97
|
+
usageCacheTtlMs: 30000
|
|
98
|
+
};
|
|
99
|
+
export function loadConfig() {
|
|
100
|
+
migrateLegacyConfig();
|
|
101
|
+
if (!existsSync(CONFIG_PATH)) {
|
|
102
|
+
log.debug("config not found, using defaults at", CONFIG_PATH);
|
|
103
|
+
return structuredClone(DEFAULT_CONFIG);
|
|
104
|
+
}
|
|
105
|
+
try {
|
|
106
|
+
ensurePrivateDirectory(dirname(CONFIG_PATH));
|
|
107
|
+
ensurePrivateFile(CONFIG_PATH);
|
|
108
|
+
const raw = JSON.parse(readFileSync(CONFIG_PATH, "utf8"));
|
|
109
|
+
return normalizeConfig(raw);
|
|
110
|
+
} catch (err) {
|
|
111
|
+
log.error("failed to read config:", err, "— using defaults");
|
|
112
|
+
return structuredClone(DEFAULT_CONFIG);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
export function saveConfigAtPath(cfg, configPath = CONFIG_PATH) {
|
|
116
|
+
ensurePrivateDirectory(dirname(configPath));
|
|
117
|
+
const temporaryPath = `${configPath}.${process.pid}.${randomUUID()}.tmp`;
|
|
118
|
+
let fileDescriptor;
|
|
119
|
+
try {
|
|
120
|
+
fileDescriptor = openSync(temporaryPath, "wx", 384);
|
|
121
|
+
writeFileSync(fileDescriptor, JSON.stringify(cfg, null, 2) + `
|
|
122
|
+
`, {
|
|
123
|
+
encoding: "utf8"
|
|
124
|
+
});
|
|
125
|
+
fsyncSync(fileDescriptor);
|
|
126
|
+
closeSync(fileDescriptor);
|
|
127
|
+
fileDescriptor = undefined;
|
|
128
|
+
renameSync(temporaryPath, configPath);
|
|
129
|
+
ensurePrivateFile(configPath);
|
|
130
|
+
} finally {
|
|
131
|
+
if (fileDescriptor !== undefined)
|
|
132
|
+
closeSync(fileDescriptor);
|
|
133
|
+
rmSync(temporaryPath, { force: true });
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
export function saveConfig(cfg) {
|
|
137
|
+
saveConfigAtPath(cfg);
|
|
138
|
+
log.info("config saved to", CONFIG_PATH);
|
|
139
|
+
}
|
|
140
|
+
function normalizeConfig(raw) {
|
|
141
|
+
if (!raw || typeof raw !== "object")
|
|
142
|
+
return structuredClone(DEFAULT_CONFIG);
|
|
143
|
+
const r = raw;
|
|
144
|
+
const merged = structuredClone(DEFAULT_CONFIG);
|
|
145
|
+
const proxyBlock = r.proxy ?? {};
|
|
146
|
+
merged.proxy.endpoint = typeof proxyBlock.endpoint === "string" ? proxyBlock.endpoint : merged.proxy.endpoint;
|
|
147
|
+
merged.proxy.apiKey = typeof proxyBlock.apiKey === "string" ? proxyBlock.apiKey : "";
|
|
148
|
+
if (typeof proxyBlock.usageKey === "string")
|
|
149
|
+
merged.proxy.usageKey = proxyBlock.usageKey;
|
|
150
|
+
if (typeof proxyBlock.providerPrefix === "string" && proxyBlock.providerPrefix.trim()) {
|
|
151
|
+
merged.proxy.providerPrefix = proxyBlock.providerPrefix.trim();
|
|
152
|
+
}
|
|
153
|
+
if (r.builtinProviders && typeof r.builtinProviders === "object") {
|
|
154
|
+
merged.builtinProviders = r.builtinProviders;
|
|
155
|
+
}
|
|
156
|
+
if (r.customProviders && typeof r.customProviders === "object") {
|
|
157
|
+
merged.customProviders = r.customProviders;
|
|
158
|
+
}
|
|
159
|
+
if (Array.isArray(r.discoveryExcludes)) {
|
|
160
|
+
merged.discoveryExcludes = r.discoveryExcludes.filter((x) => typeof x === "string");
|
|
161
|
+
}
|
|
162
|
+
if (r.overrides && typeof r.overrides === "object") {
|
|
163
|
+
merged.overrides = r.overrides;
|
|
164
|
+
}
|
|
165
|
+
if (typeof r.refreshIntervalMinutes === "number")
|
|
166
|
+
merged.refreshIntervalMinutes = r.refreshIntervalMinutes;
|
|
167
|
+
if (typeof r.usageCacheTtlMs === "number")
|
|
168
|
+
merged.usageCacheTtlMs = r.usageCacheTtlMs;
|
|
169
|
+
return merged;
|
|
170
|
+
}
|
|
171
|
+
export function resolveConfigValue(raw) {
|
|
172
|
+
if (!raw)
|
|
173
|
+
return "";
|
|
174
|
+
const value = raw.trim();
|
|
175
|
+
if (value.startsWith("!")) {
|
|
176
|
+
try {
|
|
177
|
+
return execSync(value.slice(1), {
|
|
178
|
+
encoding: "utf8",
|
|
179
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
180
|
+
}).trim();
|
|
181
|
+
} catch (err) {
|
|
182
|
+
const status = err?.status;
|
|
183
|
+
const suffix = typeof status === "number" ? ` (exit ${status})` : "";
|
|
184
|
+
log.warn(`failed to resolve "!" config value${suffix}`);
|
|
185
|
+
return "";
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
if (value.startsWith("$")) {
|
|
189
|
+
return process.env[value.slice(1)] ?? "";
|
|
190
|
+
}
|
|
191
|
+
return value;
|
|
192
|
+
}
|
|
@@ -1 +1,46 @@
|
|
|
1
|
-
import{existsSync
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
const MODELS_JSON = join(homedir(), ".dm", "models.json");
|
|
5
|
+
const AUTH_JSON = join(homedir(), ".dm", "auth.json");
|
|
6
|
+
function safeRead(path) {
|
|
7
|
+
if (!existsSync(path))
|
|
8
|
+
return null;
|
|
9
|
+
try {
|
|
10
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
11
|
+
} catch {
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
export function detectConflicts(cfg) {
|
|
16
|
+
const out = [];
|
|
17
|
+
const providersInCfg = new Set([
|
|
18
|
+
...Object.keys(cfg.builtinProviders),
|
|
19
|
+
...Object.keys(cfg.customProviders)
|
|
20
|
+
]);
|
|
21
|
+
const models = safeRead(MODELS_JSON);
|
|
22
|
+
if (models && typeof models === "object") {
|
|
23
|
+
for (const name of Object.keys(models)) {
|
|
24
|
+
if (providersInCfg.has(name)) {
|
|
25
|
+
out.push({
|
|
26
|
+
kind: "models.json",
|
|
27
|
+
provider: name,
|
|
28
|
+
detail: `provider "${name}" has user overrides in ~/.dm/models.json which DM merges on top of our registration`
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
const auth = safeRead(AUTH_JSON);
|
|
34
|
+
if (auth && typeof auth === "object") {
|
|
35
|
+
for (const name of Object.keys(auth)) {
|
|
36
|
+
if (providersInCfg.has(name)) {
|
|
37
|
+
out.push({
|
|
38
|
+
kind: "auth.json",
|
|
39
|
+
provider: name,
|
|
40
|
+
detail: `provider "${name}" has user-set credentials in ~/.dm/auth.json which take priority over our apiKey`
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return out;
|
|
46
|
+
}
|
|
@@ -1 +1,190 @@
|
|
|
1
|
-
|
|
1
|
+
import {
|
|
2
|
+
classifyCustom,
|
|
3
|
+
isExcluded,
|
|
4
|
+
modelDefaults,
|
|
5
|
+
normalizeSuggestedProvider,
|
|
6
|
+
reasoningFromId
|
|
7
|
+
} from "./compat.js";
|
|
8
|
+
import { discoveryCacheScope, writeDiscoveryCache } from "./cache.js";
|
|
9
|
+
import { log } from "./log.js";
|
|
10
|
+
export const PLUGIN_USER_AGENT = "dm-cliproxy/0.3.5";
|
|
11
|
+
const REQUEST_TIMEOUT_MS = 5000;
|
|
12
|
+
async function fetchWithTimeout(url, init) {
|
|
13
|
+
const ctrl = new AbortController;
|
|
14
|
+
const timer = setTimeout(() => ctrl.abort(new Error("timeout")), REQUEST_TIMEOUT_MS);
|
|
15
|
+
try {
|
|
16
|
+
return await fetch(url, { ...init, signal: ctrl.signal });
|
|
17
|
+
} finally {
|
|
18
|
+
clearTimeout(timer);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
function discoveryUrl(endpoint) {
|
|
22
|
+
return new URL("/.well-known/pi", new URL(endpoint).origin).toString();
|
|
23
|
+
}
|
|
24
|
+
async function tryWellKnown(cfg) {
|
|
25
|
+
const url = discoveryUrl(cfg.proxy.endpoint);
|
|
26
|
+
let resp;
|
|
27
|
+
try {
|
|
28
|
+
resp = await fetchWithTimeout(url, {
|
|
29
|
+
headers: { "User-Agent": PLUGIN_USER_AGENT, Accept: "application/json" }
|
|
30
|
+
});
|
|
31
|
+
} catch (err) {
|
|
32
|
+
log.debug("well-known fetch failed:", err.message, "— falling back to /v1/models");
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
if (!resp.ok) {
|
|
36
|
+
log.debug(`well-known returned ${resp.status} — falling back to /v1/models`);
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
let body;
|
|
40
|
+
try {
|
|
41
|
+
body = await resp.json();
|
|
42
|
+
} catch {
|
|
43
|
+
log.debug("well-known returned non-JSON — falling back to /v1/models");
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
if (!body || body.schemaVersion !== 1) {
|
|
47
|
+
log.debug("well-known schemaVersion != 1 — falling back to /v1/models");
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
const builtin = [];
|
|
51
|
+
const builtinProviders = body.builtinProviders ?? {};
|
|
52
|
+
for (const [name, p] of Object.entries(builtinProviders)) {
|
|
53
|
+
if (!p || !Array.isArray(p.models))
|
|
54
|
+
continue;
|
|
55
|
+
const models = p.models.map((m) => ({
|
|
56
|
+
id: String(m.id),
|
|
57
|
+
name: typeof m.name === "string" ? m.name : String(m.id),
|
|
58
|
+
reasoning: Boolean(m.reasoning ?? reasoningFromId(String(m.id))),
|
|
59
|
+
contextWindow: typeof m.contextWindow === "number" ? m.contextWindow : 200000,
|
|
60
|
+
maxTokens: typeof m.maxTokens === "number" ? m.maxTokens : 16000,
|
|
61
|
+
cost: m.cost ?? { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }
|
|
62
|
+
}));
|
|
63
|
+
builtin.push({ name, api: p.api ?? "openai-responses", models });
|
|
64
|
+
}
|
|
65
|
+
const customPool = (Array.isArray(body.customModelPool) ? body.customModelPool : []).map((m) => ({
|
|
66
|
+
id: String(m.id),
|
|
67
|
+
name: typeof m.name === "string" ? m.name : String(m.id),
|
|
68
|
+
reasoning: Boolean(m.reasoning ?? reasoningFromId(String(m.id))),
|
|
69
|
+
contextWindow: typeof m.contextWindow === "number" ? m.contextWindow : 128000,
|
|
70
|
+
maxTokens: typeof m.maxTokens === "number" ? m.maxTokens : 16000,
|
|
71
|
+
cost: m.cost ?? { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
72
|
+
api: m.api ?? "openai-completions",
|
|
73
|
+
suggestedProvider: normalizeSuggestedProvider(typeof m.suggestedProviderName === "string" ? m.suggestedProviderName : "misc", cfg.proxy.providerPrefix),
|
|
74
|
+
ownedBy: typeof m.owned_by === "string" ? m.owned_by : ""
|
|
75
|
+
}));
|
|
76
|
+
return {
|
|
77
|
+
source: "well-known",
|
|
78
|
+
upstreamVersion: typeof body?.upstream?.upstreamVersion === "string" ? body.upstream.upstreamVersion : null,
|
|
79
|
+
builtinProviders: builtin,
|
|
80
|
+
customPool,
|
|
81
|
+
serverDiscoveryExcludes: Array.isArray(body.discoveryExcludes) ? body.discoveryExcludes.filter((s) => typeof s === "string") : [],
|
|
82
|
+
upstreamTotal: typeof body?.counts?.upstreamTotal === "number" ? body.counts.upstreamTotal : 0
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
async function fetchRawModels(cfg, resolvedKey) {
|
|
86
|
+
const url = new URL("/v1/models", new URL(cfg.proxy.endpoint).origin).toString();
|
|
87
|
+
const resp = await fetchWithTimeout(url, {
|
|
88
|
+
headers: {
|
|
89
|
+
Authorization: `Bearer ${resolvedKey}`,
|
|
90
|
+
Accept: "application/json",
|
|
91
|
+
"User-Agent": PLUGIN_USER_AGENT
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
if (!resp.ok) {
|
|
95
|
+
throw new Error(`/v1/models returned ${resp.status}`);
|
|
96
|
+
}
|
|
97
|
+
const body = await resp.json();
|
|
98
|
+
if (!body?.data || !Array.isArray(body.data))
|
|
99
|
+
return [];
|
|
100
|
+
return body.data.map((m) => ({
|
|
101
|
+
id: typeof m.id === "string" ? m.id : "",
|
|
102
|
+
owned_by: typeof m.owned_by === "string" ? m.owned_by : ""
|
|
103
|
+
})).filter((m) => m.id);
|
|
104
|
+
}
|
|
105
|
+
function classifyLocally(raw, cfg) {
|
|
106
|
+
const excludes = cfg.discoveryExcludes;
|
|
107
|
+
const builtinByName = new Map;
|
|
108
|
+
const customPool = [];
|
|
109
|
+
let upstreamTotal = 0;
|
|
110
|
+
for (const m of raw) {
|
|
111
|
+
upstreamTotal++;
|
|
112
|
+
if (isExcluded(m.id, excludes))
|
|
113
|
+
continue;
|
|
114
|
+
if (m.owned_by === "openai") {
|
|
115
|
+
const entry = modelDefaults(m.id);
|
|
116
|
+
pushBuiltin(builtinByName, "openai", "openai-responses", entryToDiscovery(entry));
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
if (m.owned_by === "anthropic") {
|
|
120
|
+
const entry = modelDefaults(m.id);
|
|
121
|
+
pushBuiltin(builtinByName, "anthropic", "anthropic-messages", entryToDiscovery(entry));
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
const { slug, api } = classifyCustom(m.owned_by, cfg.proxy.providerPrefix);
|
|
125
|
+
const base = modelDefaults(m.id);
|
|
126
|
+
customPool.push({
|
|
127
|
+
id: m.id,
|
|
128
|
+
name: base.name ?? m.id,
|
|
129
|
+
reasoning: base.reasoning ?? false,
|
|
130
|
+
contextWindow: base.contextWindow ?? 128000,
|
|
131
|
+
maxTokens: base.maxTokens ?? 16000,
|
|
132
|
+
cost: base.cost ?? { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
133
|
+
api,
|
|
134
|
+
suggestedProvider: slug,
|
|
135
|
+
ownedBy: m.owned_by
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
return {
|
|
139
|
+
source: "v1-models",
|
|
140
|
+
upstreamVersion: null,
|
|
141
|
+
builtinProviders: Array.from(builtinByName.values()),
|
|
142
|
+
customPool,
|
|
143
|
+
serverDiscoveryExcludes: [],
|
|
144
|
+
upstreamTotal
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
function pushBuiltin(map, name, api, entry) {
|
|
148
|
+
let p = map.get(name);
|
|
149
|
+
if (!p) {
|
|
150
|
+
p = { name, api, models: [] };
|
|
151
|
+
map.set(name, p);
|
|
152
|
+
}
|
|
153
|
+
p.models.push(entry);
|
|
154
|
+
}
|
|
155
|
+
function entryToDiscovery(base) {
|
|
156
|
+
return {
|
|
157
|
+
id: base.id,
|
|
158
|
+
name: base.name ?? base.id,
|
|
159
|
+
reasoning: base.reasoning ?? false,
|
|
160
|
+
contextWindow: base.contextWindow ?? 128000,
|
|
161
|
+
maxTokens: base.maxTokens ?? 16000,
|
|
162
|
+
cost: base.cost ?? { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
export async function fetchDiscovery(cfg, resolvedKey) {
|
|
166
|
+
const cacheScope = discoveryCacheScope(cfg.proxy.endpoint, resolvedKey);
|
|
167
|
+
const wk = await tryWellKnown(cfg);
|
|
168
|
+
if (wk) {
|
|
169
|
+
log.debug(`discovery via /.well-known/pi: ${wk.builtinProviders.length} builtin, ${wk.customPool.length} custom`);
|
|
170
|
+
writeDiscoveryCache(wk, cacheScope);
|
|
171
|
+
return wk;
|
|
172
|
+
}
|
|
173
|
+
if (!resolvedKey) {
|
|
174
|
+
throw new Error("well-known unavailable AND proxy apiKey is empty — cannot fall back to /v1/models. Set proxy.apiKey in config.");
|
|
175
|
+
}
|
|
176
|
+
const raw = await fetchRawModels(cfg, resolvedKey);
|
|
177
|
+
const d = classifyLocally(raw, cfg);
|
|
178
|
+
log.debug(`discovery via /v1/models: ${d.builtinProviders.length} builtin, ${d.customPool.length} custom`);
|
|
179
|
+
writeDiscoveryCache(d, cacheScope);
|
|
180
|
+
return d;
|
|
181
|
+
}
|
|
182
|
+
export function discoveryToIdSet(d) {
|
|
183
|
+
const out = new Set;
|
|
184
|
+
for (const p of d.builtinProviders)
|
|
185
|
+
for (const m of p.models)
|
|
186
|
+
out.add(m.id);
|
|
187
|
+
for (const m of d.customPool)
|
|
188
|
+
out.add(m.id);
|
|
189
|
+
return out;
|
|
190
|
+
}
|
|
@@ -1 +1,41 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { PLUGIN_USER_AGENT } from "./fetch-models.js";
|
|
2
|
+
import { log } from "./log.js";
|
|
3
|
+
const REQUEST_TIMEOUT_MS = 15000;
|
|
4
|
+
let cache = null;
|
|
5
|
+
export function clearUsageCache() {
|
|
6
|
+
cache = null;
|
|
7
|
+
}
|
|
8
|
+
export async function fetchUsage(cfg, resolvedUsageKey, opts = {}) {
|
|
9
|
+
if (!opts.force && cache && Date.now() - cache.fetchedAt < cfg.usageCacheTtlMs) {
|
|
10
|
+
return cache.doc;
|
|
11
|
+
}
|
|
12
|
+
if (!resolvedUsageKey) {
|
|
13
|
+
throw new Error("usage key not configured; set proxy.usageKey in config and rerun");
|
|
14
|
+
}
|
|
15
|
+
const url = new URL("/api/usage", new URL(cfg.proxy.endpoint).origin).toString();
|
|
16
|
+
const ctrl = new AbortController;
|
|
17
|
+
const timer = setTimeout(() => ctrl.abort(new Error("timeout")), REQUEST_TIMEOUT_MS);
|
|
18
|
+
let resp;
|
|
19
|
+
try {
|
|
20
|
+
resp = await fetch(url, {
|
|
21
|
+
headers: {
|
|
22
|
+
"X-Plugin-Key": resolvedUsageKey,
|
|
23
|
+
Accept: "application/json",
|
|
24
|
+
"User-Agent": PLUGIN_USER_AGENT
|
|
25
|
+
},
|
|
26
|
+
signal: ctrl.signal
|
|
27
|
+
});
|
|
28
|
+
} finally {
|
|
29
|
+
clearTimeout(timer);
|
|
30
|
+
}
|
|
31
|
+
if (!resp.ok) {
|
|
32
|
+
throw new Error(`/api/usage returned ${resp.status}`);
|
|
33
|
+
}
|
|
34
|
+
const body = await resp.json();
|
|
35
|
+
if (!body || body.schemaVersion !== 1 || !Array.isArray(body.accounts)) {
|
|
36
|
+
throw new Error("/api/usage returned unexpected payload shape");
|
|
37
|
+
}
|
|
38
|
+
cache = { fetchedAt: Date.now(), doc: body };
|
|
39
|
+
log.debug("usage fetched, accounts:", body.accounts.length);
|
|
40
|
+
return body;
|
|
41
|
+
}
|
|
@@ -1 +1,23 @@
|
|
|
1
|
-
|
|
1
|
+
const TAG = "[dm-cliproxy]";
|
|
2
|
+
let quiet = false;
|
|
3
|
+
export function setLogQuiet(v) {
|
|
4
|
+
quiet = v;
|
|
5
|
+
}
|
|
6
|
+
export const log = {
|
|
7
|
+
info(...args) {
|
|
8
|
+
if (!quiet)
|
|
9
|
+
console.log(TAG, ...args);
|
|
10
|
+
},
|
|
11
|
+
warn(...args) {
|
|
12
|
+
if (!quiet)
|
|
13
|
+
console.warn(TAG, ...args);
|
|
14
|
+
},
|
|
15
|
+
error(...args) {
|
|
16
|
+
if (!quiet)
|
|
17
|
+
console.error(TAG, ...args);
|
|
18
|
+
},
|
|
19
|
+
debug(...args) {
|
|
20
|
+
if (!quiet && (process.env.DM_CLIPROXY_DEBUG || process.env.PI_CLIPROXYAPI_DEBUG))
|
|
21
|
+
console.log(TAG, "[debug]", ...args);
|
|
22
|
+
}
|
|
23
|
+
};
|
|
@@ -1 +1,77 @@
|
|
|
1
|
-
|
|
1
|
+
const GAUGE_ICON = "\uDB81\uDE27";
|
|
2
|
+
const BRAILLE = ["⡀", "⠂", "⣀", "⣄", "⣤", "⣶", "⣷", "⣾"];
|
|
3
|
+
export function providerToUsageKey(piProvider) {
|
|
4
|
+
if (piProvider === "anthropic")
|
|
5
|
+
return "claude";
|
|
6
|
+
if (piProvider === "openai")
|
|
7
|
+
return "codex";
|
|
8
|
+
return null;
|
|
9
|
+
}
|
|
10
|
+
function isLiveAccount(a) {
|
|
11
|
+
return !a.disabled && !a.unavailable && a.supported && Array.isArray(a.groups) && a.groups.length > 0;
|
|
12
|
+
}
|
|
13
|
+
function is5hGroup(g) {
|
|
14
|
+
const s = `${g.id} ${g.label ?? ""}`.toLowerCase();
|
|
15
|
+
return s.includes("5h") || s.includes("five-hour") || s.includes("five hour");
|
|
16
|
+
}
|
|
17
|
+
function is7dGroup(g) {
|
|
18
|
+
const s = `${g.id} ${g.label ?? ""}`.toLowerCase();
|
|
19
|
+
return s.includes("7d") || s.includes("seven-day") || s.includes("seven day");
|
|
20
|
+
}
|
|
21
|
+
function aggregateWindow(accounts, predicate) {
|
|
22
|
+
let maxAcross = -1;
|
|
23
|
+
for (const a of accounts) {
|
|
24
|
+
if (!isLiveAccount(a))
|
|
25
|
+
continue;
|
|
26
|
+
let minWithin = 1 / 0;
|
|
27
|
+
for (const g of a.groups ?? []) {
|
|
28
|
+
if (predicate(g)) {
|
|
29
|
+
if (g.remainingFraction < minWithin)
|
|
30
|
+
minWithin = g.remainingFraction;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
if (minWithin !== 1 / 0 && minWithin > maxAcross) {
|
|
34
|
+
maxAcross = minWithin;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
if (maxAcross < 0)
|
|
38
|
+
return null;
|
|
39
|
+
return { fraction: clamp(maxAcross) };
|
|
40
|
+
}
|
|
41
|
+
function clamp(f) {
|
|
42
|
+
return Math.max(0, Math.min(1, f));
|
|
43
|
+
}
|
|
44
|
+
function brailleFor(fraction) {
|
|
45
|
+
const idx = Math.min(BRAILLE.length - 1, Math.floor(fraction * BRAILLE.length));
|
|
46
|
+
return BRAILLE[idx];
|
|
47
|
+
}
|
|
48
|
+
function colorForFraction(fraction) {
|
|
49
|
+
if (fraction >= 0.7)
|
|
50
|
+
return "success";
|
|
51
|
+
if (fraction >= 0.3)
|
|
52
|
+
return "warning";
|
|
53
|
+
return "error";
|
|
54
|
+
}
|
|
55
|
+
function pct(fraction) {
|
|
56
|
+
return Math.round(fraction * 100);
|
|
57
|
+
}
|
|
58
|
+
export function renderQuotaSegment(doc, piProvider, theme) {
|
|
59
|
+
const usageKey = providerToUsageKey(piProvider);
|
|
60
|
+
if (!usageKey)
|
|
61
|
+
return null;
|
|
62
|
+
const accounts = doc.accounts.filter((a) => a.provider === usageKey);
|
|
63
|
+
if (accounts.length === 0)
|
|
64
|
+
return null;
|
|
65
|
+
const w5 = aggregateWindow(accounts, is5hGroup);
|
|
66
|
+
const w7 = aggregateWindow(accounts, is7dGroup);
|
|
67
|
+
if (!w5 && !w7)
|
|
68
|
+
return null;
|
|
69
|
+
const parts = [theme.fg("dim", GAUGE_ICON)];
|
|
70
|
+
if (w5) {
|
|
71
|
+
parts.push(`${theme.fg("dim", "5h")} ${theme.fg(colorForFraction(w5.fraction), brailleFor(w5.fraction))} ${theme.fg(colorForFraction(w5.fraction), String(pct(w5.fraction)))}`);
|
|
72
|
+
}
|
|
73
|
+
if (w7) {
|
|
74
|
+
parts.push(`${theme.fg("dim", "7d")} ${theme.fg(colorForFraction(w7.fraction), brailleFor(w7.fraction))} ${theme.fg(colorForFraction(w7.fraction), String(pct(w7.fraction)))}`);
|
|
75
|
+
}
|
|
76
|
+
return parts.join(" ");
|
|
77
|
+
}
|
|
@@ -1 +1,50 @@
|
|
|
1
|
-
import{visibleWidth
|
|
1
|
+
import { visibleWidth } from "@duckmind/dm-tui";
|
|
2
|
+
import { pad } from "./ui-picker/render-text.js";
|
|
3
|
+
const SIDE = "│";
|
|
4
|
+
const TL = "╭";
|
|
5
|
+
const TR = "╮";
|
|
6
|
+
const BL = "╰";
|
|
7
|
+
const BR = "╯";
|
|
8
|
+
const HR = "─";
|
|
9
|
+
export function frame(theme, opts) {
|
|
10
|
+
const w = Math.max(4, opts.width);
|
|
11
|
+
return [
|
|
12
|
+
drawTop(theme, opts.title, opts.titleColor ?? "accent", w),
|
|
13
|
+
...opts.lines.map((line) => drawBody(theme, line, w)),
|
|
14
|
+
drawBottom(theme, opts.footer, w)
|
|
15
|
+
];
|
|
16
|
+
}
|
|
17
|
+
function drawTop(theme, title, color, width) {
|
|
18
|
+
const t = title ?? "";
|
|
19
|
+
const titleVis = visibleWidth(t);
|
|
20
|
+
const rest = Math.max(0, width - 3 - titleVis);
|
|
21
|
+
const titleColoured = t ? theme.bold(theme.fg(color, t)) : "";
|
|
22
|
+
const body = `${TL}${HR}${titleColoured}${HR.repeat(rest)}${TR}`;
|
|
23
|
+
return theme.fg("borderAccent", body);
|
|
24
|
+
}
|
|
25
|
+
function drawBody(theme, content, width) {
|
|
26
|
+
const inner = Math.max(0, width - 2);
|
|
27
|
+
const side = theme.fg("borderAccent", SIDE);
|
|
28
|
+
return `${side}${pad(content, inner)}${side}`;
|
|
29
|
+
}
|
|
30
|
+
function drawBottom(theme, footer, width) {
|
|
31
|
+
const start = `${BL}${HR}`;
|
|
32
|
+
const end = `${BR}`;
|
|
33
|
+
const fixedCells = 3;
|
|
34
|
+
if (!footer || !footer.hint && !footer.badge) {
|
|
35
|
+
const rest = Math.max(0, width - fixedCells);
|
|
36
|
+
return theme.fg("borderAccent", `${start}${HR.repeat(rest)}${end}`);
|
|
37
|
+
}
|
|
38
|
+
const hint = footer.hint ?? "";
|
|
39
|
+
const badge = footer.badge ?? "";
|
|
40
|
+
const hintVis = visibleWidth(hint);
|
|
41
|
+
const badgeVis = visibleWidth(badge);
|
|
42
|
+
const rest = Math.max(0, width - fixedCells - hintVis - badgeVis);
|
|
43
|
+
const left = theme.fg("dim", hint);
|
|
44
|
+
const right = badge ? theme.fg("muted", badge) : "";
|
|
45
|
+
const fill = HR.repeat(rest);
|
|
46
|
+
return `${theme.fg("borderAccent", start)}${left}${theme.fg("borderAccent", fill)}${right}${theme.fg("borderAccent", end)}`;
|
|
47
|
+
}
|
|
48
|
+
export function frameInner(width) {
|
|
49
|
+
return Math.max(0, width - 2);
|
|
50
|
+
}
|