@duckmind/dm-darwin-x64 0.32.9 → 0.33.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.
Files changed (95) hide show
  1. package/dm +0 -0
  2. package/extensions/.dm-extensions.json +14 -65
  3. package/extensions/dm-9router-ext/README.md +142 -0
  4. package/extensions/dm-9router-ext/package.json +45 -0
  5. package/extensions/dm-9router-ext/src/index.ts +541 -0
  6. package/extensions/dm-9router-ext/tsconfig.json +17 -0
  7. package/extensions/dm-subagents/package-lock.json +3 -3
  8. package/extensions/dm-tasks/node_modules/.package-lock.json +3 -3
  9. package/extensions/dm-tasks/node_modules/typebox/build/type/script/mapping.d.mts +2 -2
  10. package/extensions/dm-tasks/node_modules/typebox/build/type/script/parser.d.mts +2 -2
  11. package/extensions/dm-tasks/node_modules/typebox/build/type/script/parser.mjs +2 -2
  12. package/extensions/dm-tasks/node_modules/typebox/build/type/types/number.d.mts +1 -1
  13. package/extensions/dm-tasks/node_modules/typebox/build/type/types/number.mjs +1 -1
  14. package/extensions/dm-tasks/node_modules/typebox/build/type/types/record.d.mts +1 -1
  15. package/extensions/dm-tasks/node_modules/typebox/package.json +1 -1
  16. package/extensions/dm-tasks/package-lock.json +3 -3
  17. package/package.json +1 -1
  18. package/theme/theme-alps.json +93 -0
  19. package/extensions/dm-chime/README.md +0 -11
  20. package/extensions/dm-chime/docs/protocols.md +0 -107
  21. package/extensions/dm-chime/index.ts +0 -205
  22. package/extensions/dm-chime/package.json +0 -33
  23. package/extensions/dm-phone/README.md +0 -24
  24. package/extensions/dm-phone/index.ts +0 -12
  25. package/extensions/dm-phone/node_modules/.package-lock.json +0 -29
  26. package/extensions/dm-phone/node_modules/ws/LICENSE +0 -20
  27. package/extensions/dm-phone/node_modules/ws/README.md +0 -548
  28. package/extensions/dm-phone/node_modules/ws/browser.js +0 -8
  29. package/extensions/dm-phone/node_modules/ws/index.js +0 -22
  30. package/extensions/dm-phone/node_modules/ws/lib/buffer-util.js +0 -131
  31. package/extensions/dm-phone/node_modules/ws/lib/constants.js +0 -19
  32. package/extensions/dm-phone/node_modules/ws/lib/event-target.js +0 -292
  33. package/extensions/dm-phone/node_modules/ws/lib/extension.js +0 -203
  34. package/extensions/dm-phone/node_modules/ws/lib/limiter.js +0 -55
  35. package/extensions/dm-phone/node_modules/ws/lib/permessage-deflate.js +0 -528
  36. package/extensions/dm-phone/node_modules/ws/lib/receiver.js +0 -760
  37. package/extensions/dm-phone/node_modules/ws/lib/sender.js +0 -607
  38. package/extensions/dm-phone/node_modules/ws/lib/stream.js +0 -161
  39. package/extensions/dm-phone/node_modules/ws/lib/subprotocol.js +0 -62
  40. package/extensions/dm-phone/node_modules/ws/lib/validation.js +0 -152
  41. package/extensions/dm-phone/node_modules/ws/lib/websocket-server.js +0 -562
  42. package/extensions/dm-phone/node_modules/ws/lib/websocket.js +0 -1407
  43. package/extensions/dm-phone/node_modules/ws/package.json +0 -70
  44. package/extensions/dm-phone/node_modules/ws/wrapper.mjs +0 -21
  45. package/extensions/dm-phone/package-lock.json +0 -66
  46. package/extensions/dm-phone/package.json +0 -35
  47. package/extensions/dm-phone/phone-session-pool.ts +0 -8
  48. package/extensions/dm-phone/public/app/attachments.js +0 -233
  49. package/extensions/dm-phone/public/app/autocomplete-controller.js +0 -81
  50. package/extensions/dm-phone/public/app/autocomplete.js +0 -135
  51. package/extensions/dm-phone/public/app/bindings.js +0 -178
  52. package/extensions/dm-phone/public/app/command-catalog.js +0 -76
  53. package/extensions/dm-phone/public/app/commands.js +0 -376
  54. package/extensions/dm-phone/public/app/constants.js +0 -60
  55. package/extensions/dm-phone/public/app/formatters.js +0 -131
  56. package/extensions/dm-phone/public/app/handlers.js +0 -442
  57. package/extensions/dm-phone/public/app/main.js +0 -6
  58. package/extensions/dm-phone/public/app/markdown.js +0 -105
  59. package/extensions/dm-phone/public/app/messages.js +0 -418
  60. package/extensions/dm-phone/public/app/sheet-actions.js +0 -113
  61. package/extensions/dm-phone/public/app/sheet-navigation.js +0 -19
  62. package/extensions/dm-phone/public/app/sheets-view.js +0 -287
  63. package/extensions/dm-phone/public/app/state.js +0 -95
  64. package/extensions/dm-phone/public/app/tool-rendering.js +0 -562
  65. package/extensions/dm-phone/public/app/transport.js +0 -176
  66. package/extensions/dm-phone/public/app/ui.js +0 -417
  67. package/extensions/dm-phone/public/app.js +0 -1
  68. package/extensions/dm-phone/public/icon.svg +0 -15
  69. package/extensions/dm-phone/public/index.html +0 -146
  70. package/extensions/dm-phone/public/manifest.webmanifest +0 -17
  71. package/extensions/dm-phone/public/styles.css +0 -1139
  72. package/extensions/dm-phone/public/sw.js +0 -78
  73. package/extensions/dm-phone/src/extension/duckmind-models.js +0 -264
  74. package/extensions/dm-phone/src/extension/phone-args.ts +0 -121
  75. package/extensions/dm-phone/src/extension/phone-paths.ts +0 -250
  76. package/extensions/dm-phone/src/extension/phone-quota.ts +0 -188
  77. package/extensions/dm-phone/src/extension/phone-runtime.ts +0 -154
  78. package/extensions/dm-phone/src/extension/phone-server-runtime.ts +0 -1217
  79. package/extensions/dm-phone/src/extension/phone-sessions.ts +0 -139
  80. package/extensions/dm-phone/src/extension/phone-static.ts +0 -30
  81. package/extensions/dm-phone/src/extension/phone-tailscale.ts +0 -148
  82. package/extensions/dm-phone/src/extension/phone-theme.ts +0 -85
  83. package/extensions/dm-phone/src/extension/register-phone-child-extension.ts +0 -112
  84. package/extensions/dm-phone/src/extension/register-phone-extension.ts +0 -106
  85. package/extensions/dm-phone/src/extension/types.ts +0 -73
  86. package/extensions/dm-phone/src/session-pool/parent-session-worker.ts +0 -882
  87. package/extensions/dm-phone/src/session-pool/session-pool.ts +0 -470
  88. package/extensions/dm-phone/src/session-pool/session-worker.ts +0 -739
  89. package/extensions/dm-phone/src/session-pool/types.ts +0 -111
  90. package/extensions/dm-phone/src/session-pool/utils.ts +0 -23
  91. package/extensions/dm-phone/test/duckmind-models.test.js +0 -147
  92. package/extensions/dm-thinking-timer/LICENSE +0 -21
  93. package/extensions/dm-thinking-timer/README.md +0 -7
  94. package/extensions/dm-thinking-timer/package.json +0 -20
  95. package/extensions/dm-thinking-timer/thinking-timer.ts +0 -250
@@ -1,78 +0,0 @@
1
- const CACHE = "dm-phone-v17";
2
- const ASSETS = [
3
- "/",
4
- "/styles.css",
5
- "/app.js",
6
- "/app/attachments.js",
7
- "/app/autocomplete-controller.js",
8
- "/app/autocomplete.js",
9
- "/app/bindings.js",
10
- "/app/command-catalog.js",
11
- "/app/commands.js",
12
- "/app/constants.js",
13
- "/app/formatters.js",
14
- "/app/handlers.js",
15
- "/app/main.js",
16
- "/app/markdown.js",
17
- "/app/messages.js",
18
- "/app/sheet-actions.js",
19
- "/app/sheet-navigation.js",
20
- "/app/sheets-view.js",
21
- "/app/state.js",
22
- "/app/tool-rendering.js",
23
- "/app/transport.js",
24
- "/app/ui.js",
25
- "/manifest.webmanifest",
26
- "/icon.svg",
27
- ];
28
- const APP_SHELL = new Set(ASSETS);
29
-
30
- self.addEventListener("install", (event) => {
31
- event.waitUntil(caches.open(CACHE).then((cache) => cache.addAll(ASSETS)));
32
- self.skipWaiting();
33
- });
34
-
35
- self.addEventListener("activate", (event) => {
36
- event.waitUntil(
37
- caches.keys().then((keys) => Promise.all(keys.filter((key) => key !== CACHE).map((key) => caches.delete(key)))),
38
- );
39
- self.clients.claim();
40
- });
41
-
42
- async function updateCache(request) {
43
- const response = await fetch(request);
44
- if (response.ok) {
45
- const copy = response.clone();
46
- caches.open(CACHE).then((cache) => cache.put(request, copy));
47
- }
48
- return response;
49
- }
50
-
51
- self.addEventListener("fetch", (event) => {
52
- const { request } = event;
53
- if (request.method !== "GET") return;
54
-
55
- const url = new URL(request.url);
56
- if (url.origin !== self.location.origin) return;
57
- if (url.pathname.startsWith("/api/") || url.pathname === "/ws") return;
58
-
59
- const useNetworkFirst = request.mode === "navigate" || APP_SHELL.has(url.pathname);
60
-
61
- if (useNetworkFirst) {
62
- event.respondWith(
63
- updateCache(request).catch(async () => {
64
- const cached = await caches.match(request);
65
- if (cached) return cached;
66
- return caches.match("/");
67
- }),
68
- );
69
- return;
70
- }
71
-
72
- event.respondWith(
73
- caches.match(request).then((cached) => {
74
- if (cached) return cached;
75
- return updateCache(request);
76
- }),
77
- );
78
- });
@@ -1,264 +0,0 @@
1
- import { existsSync, readFileSync } from "node:fs";
2
- import { join } from "node:path";
3
-
4
- const DUCKMIND_PROVIDER = "duckmind";
5
- const OPENROUTER_PROVIDER = "openrouter";
6
- const OPENAI_CODEX_PROVIDER = "openai-codex";
7
-
8
- const DUCKMIND_PRESETS = [
9
- {
10
- alias: "free",
11
- envKey: "DM_FREE_MODEL",
12
- fallbackModelId: "@preset/free",
13
- displayName: "Free",
14
- },
15
- {
16
- alias: "auto",
17
- envKey: "DM_AUTO_MODEL",
18
- fallbackModelId: "@preset/auto",
19
- displayName: "Auto",
20
- },
21
- {
22
- alias: "lite",
23
- envKey: "DM_LITE_MODEL",
24
- fallbackModelId: "@preset/lite",
25
- displayName: "Lite",
26
- },
27
- {
28
- alias: "smart",
29
- envKey: "DM_SMART_MODEL",
30
- fallbackModelId: "@preset/smart",
31
- displayName: "Smart",
32
- },
33
- {
34
- alias: "deep",
35
- envKey: "DM_DEEP_MODEL",
36
- fallbackModelId: "@preset/deep",
37
- displayName: "Deep",
38
- },
39
- {
40
- alias: "image2",
41
- envKey: "DM_IMAGE2_MODEL",
42
- fallbackModelId: "@preset/image2",
43
- displayName: "Image2",
44
- },
45
- ];
46
-
47
- const DUCKMIND_ULTRA = {
48
- alias: "ultra",
49
- actualModelId: "gpt-5.5",
50
- displayName: "Ultra",
51
- };
52
-
53
- function normalize(value) {
54
- return String(value ?? "").trim().toLowerCase();
55
- }
56
-
57
- function resolvePresetTarget(envKey, fallbackModelId) {
58
- const override = process.env[envKey]?.trim();
59
- return override || fallbackModelId;
60
- }
61
-
62
- function getAgentDir() {
63
- return process.env.DM_CODING_AGENT_DIR?.trim()
64
- || process.env.PI_CODING_AGENT_DIR?.trim()
65
- || join(process.env.HOME || process.env.USERPROFILE || process.cwd(), ".dm", "agent");
66
- }
67
-
68
- function hasUltradexAccounts() {
69
- const accountsPath = join(getAgentDir(), "codex-accounts.json");
70
- if (!existsSync(accountsPath)) {
71
- return false;
72
- }
73
-
74
- try {
75
- const parsed = JSON.parse(readFileSync(accountsPath, "utf-8"));
76
- return Array.isArray(parsed?.accounts) && parsed.accounts.length > 0;
77
- } catch {
78
- return false;
79
- }
80
- }
81
-
82
- function pickProviderModel(models, provider, actualModelId, fallbackModelId, displayName) {
83
- const providerModels = Array.isArray(models)
84
- ? models.filter((model) => normalize(model?.provider) === normalize(provider))
85
- : [];
86
- if (!providerModels.length) {
87
- return null;
88
- }
89
-
90
- const exact = providerModels.find((model) => normalize(model?.id) === normalize(actualModelId));
91
- if (exact) {
92
- return exact;
93
- }
94
-
95
- const fallback = providerModels.find((model) => normalize(model?.id) === normalize(fallbackModelId)) || providerModels[0];
96
- return {
97
- ...fallback,
98
- id: actualModelId,
99
- name: displayName,
100
- };
101
- }
102
-
103
- function finiteContextWindow(model) {
104
- const contextWindow = Number(model?.contextWindow);
105
- return Number.isFinite(contextWindow) && contextWindow > 0 ? contextWindow : undefined;
106
- }
107
-
108
- function getResolvedPresets(presets = DUCKMIND_PRESETS) {
109
- return presets.map((preset) => ({
110
- ...preset,
111
- actualModelId: resolvePresetTarget(preset.envKey, preset.fallbackModelId),
112
- }));
113
- }
114
-
115
- function toVisibleOption(model, preset) {
116
- return {
117
- provider: DUCKMIND_PROVIDER,
118
- id: preset.alias,
119
- name: preset.displayName,
120
- actualProvider: OPENROUTER_PROVIDER,
121
- actualModelId: preset.actualModelId,
122
- ...(finiteContextWindow(model) ? { contextWindow: finiteContextWindow(model) } : {}),
123
- };
124
- }
125
-
126
- export function getPhoneVisibleModels(models) {
127
- const visible = [];
128
-
129
- for (const preset of getResolvedPresets()) {
130
- const model = pickProviderModel(
131
- models,
132
- OPENROUTER_PROVIDER,
133
- preset.actualModelId,
134
- preset.fallbackModelId,
135
- preset.displayName,
136
- );
137
- if (!model) {
138
- continue;
139
- }
140
-
141
- visible.push(toVisibleOption(model, preset));
142
- }
143
-
144
- if (hasUltradexAccounts()) {
145
- const ultraModel = pickProviderModel(
146
- models,
147
- OPENAI_CODEX_PROVIDER,
148
- DUCKMIND_ULTRA.actualModelId,
149
- DUCKMIND_ULTRA.actualModelId,
150
- DUCKMIND_ULTRA.displayName,
151
- );
152
- if (ultraModel) {
153
- visible.push({
154
- provider: DUCKMIND_PROVIDER,
155
- id: DUCKMIND_ULTRA.alias,
156
- name: DUCKMIND_ULTRA.displayName,
157
- actualProvider: OPENAI_CODEX_PROVIDER,
158
- actualModelId: DUCKMIND_ULTRA.actualModelId,
159
- ...(finiteContextWindow(ultraModel) ? { contextWindow: finiteContextWindow(ultraModel) } : {}),
160
- });
161
- }
162
- }
163
-
164
- const providerModels = Array.isArray(models)
165
- ? models
166
- .filter((model) => normalize(model?.provider) !== DUCKMIND_PROVIDER)
167
- .sort((a, b) => {
168
- const providerCmp = String(a?.provider ?? "").localeCompare(String(b?.provider ?? ""));
169
- if (providerCmp !== 0) return providerCmp;
170
- return String(a?.id ?? "").localeCompare(String(b?.id ?? ""));
171
- })
172
- : [];
173
- for (const model of providerModels) {
174
- const displayProvider = normalize(model?.provider) === OPENROUTER_PROVIDER ? DUCKMIND_PROVIDER : model.provider;
175
- visible.push({
176
- provider: displayProvider,
177
- id: model.id,
178
- name: model.name || model.id,
179
- actualProvider: model.provider,
180
- actualModelId: model.id,
181
- ...(finiteContextWindow(model) ? { contextWindow: finiteContextWindow(model) } : {}),
182
- });
183
- }
184
-
185
- return visible;
186
- }
187
-
188
- export function normalizePhoneSessionModel(model) {
189
- if (!model || typeof model !== "object") {
190
- return null;
191
- }
192
-
193
- const provider = normalize(model.provider);
194
- const modelId = normalize(model.id);
195
- for (const preset of getResolvedPresets(DUCKMIND_PRESETS)) {
196
- const matchesAlias = provider === normalize(DUCKMIND_PROVIDER) && modelId === normalize(preset.alias);
197
- const matchesActual = provider === normalize(OPENROUTER_PROVIDER) && modelId === normalize(preset.actualModelId);
198
- if (!matchesAlias && !matchesActual) {
199
- continue;
200
- }
201
- return {
202
- ...model,
203
- ...toVisibleOption(model, preset),
204
- };
205
- }
206
-
207
- const matchesUltraAlias = provider === normalize(DUCKMIND_PROVIDER) && modelId === normalize(DUCKMIND_ULTRA.alias);
208
- const matchesUltraActual = provider === normalize(OPENAI_CODEX_PROVIDER) && modelId === normalize(DUCKMIND_ULTRA.actualModelId);
209
- if (hasUltradexAccounts() && (matchesUltraAlias || matchesUltraActual)) {
210
- return {
211
- ...model,
212
- provider: DUCKMIND_PROVIDER,
213
- id: DUCKMIND_ULTRA.alias,
214
- name: DUCKMIND_ULTRA.displayName,
215
- actualProvider: OPENAI_CODEX_PROVIDER,
216
- actualModelId: DUCKMIND_ULTRA.actualModelId,
217
- ...(finiteContextWindow(model) ? { contextWindow: finiteContextWindow(model) } : {}),
218
- };
219
- }
220
-
221
- return {
222
- ...model,
223
- };
224
- }
225
-
226
- export function resolvePhoneVisibleModel(models, provider, modelId) {
227
- const requestedProvider = normalize(provider);
228
- const requestedId = normalize(modelId);
229
- if (!requestedId) {
230
- return null;
231
- }
232
-
233
- for (const option of getPhoneVisibleModels(models)) {
234
- const optionProvider = normalize(option.provider);
235
- const optionId = normalize(option.id);
236
- const actualProvider = normalize(option.actualProvider);
237
- const actualModelId = normalize(option.actualModelId);
238
-
239
- const matchesAlias = requestedId === optionId && (!requestedProvider || requestedProvider === optionProvider);
240
- const matchesActual = requestedProvider === actualProvider && requestedId === actualModelId;
241
- const matchesProviderlessActual = !requestedProvider && requestedId === actualModelId;
242
- const matchesCompat = requestedProvider === optionProvider && requestedId === actualModelId;
243
-
244
- if (!matchesAlias && !matchesActual && !matchesProviderlessActual && !matchesCompat) {
245
- continue;
246
- }
247
-
248
- const preset = DUCKMIND_PRESETS.find((entry) => entry.alias === option.id);
249
- const model = pickProviderModel(
250
- models,
251
- option.actualProvider,
252
- option.actualModelId,
253
- preset?.fallbackModelId || option.actualModelId,
254
- option.name || option.id,
255
- );
256
- if (!model) {
257
- return null;
258
- }
259
-
260
- return { option, model };
261
- }
262
-
263
- return null;
264
- }
@@ -1,121 +0,0 @@
1
- import { resolve } from "node:path";
2
- import type { ParsedPhoneArgs, PhoneConfig } from "./types";
3
-
4
- export function parsePhoneStartArgs(args: string | undefined, current: PhoneConfig): ParsedPhoneArgs {
5
- const next = { ...current };
6
- let tokenSpecified = false;
7
- let idleSpecified = false;
8
-
9
- if (!args?.trim()) {
10
- return { config: next, tokenSpecified, idleSpecified };
11
- }
12
-
13
- const tokens = args.trim().split(/\s+/);
14
- let index = 0;
15
-
16
- while (index < tokens.length) {
17
- const token = tokens[index];
18
-
19
- if (token === "--port" && tokens[index + 1]) {
20
- const port = Number(tokens[index + 1]);
21
- if (Number.isFinite(port) && port > 0) next.port = port;
22
- index += 2;
23
- continue;
24
- }
25
-
26
- if (token.startsWith("--port=")) {
27
- const port = Number(token.slice(7));
28
- if (Number.isFinite(port) && port > 0) next.port = port;
29
- index += 1;
30
- continue;
31
- }
32
-
33
- if (token === "--host" && tokens[index + 1]) {
34
- next.host = tokens[index + 1];
35
- index += 2;
36
- continue;
37
- }
38
-
39
- if (token.startsWith("--host=")) {
40
- next.host = token.slice(7);
41
- index += 1;
42
- continue;
43
- }
44
-
45
- if (token === "--token" && tokens[index + 1] !== undefined) {
46
- tokenSpecified = true;
47
- next.token = tokens[index + 1] === "-" ? "" : tokens[index + 1];
48
- index += 2;
49
- continue;
50
- }
51
-
52
- if (token.startsWith("--token=")) {
53
- tokenSpecified = true;
54
- const value = token.slice(8);
55
- next.token = value === "-" ? "" : value;
56
- index += 1;
57
- continue;
58
- }
59
-
60
- if (token === "--cwd" && tokens[index + 1]) {
61
- next.cwd = resolve(tokens[index + 1]);
62
- index += 2;
63
- continue;
64
- }
65
-
66
- if (token.startsWith("--cwd=")) {
67
- next.cwd = resolve(token.slice(6));
68
- index += 1;
69
- continue;
70
- }
71
-
72
- if (token === "--idle-mins" && tokens[index + 1] !== undefined) {
73
- idleSpecified = true;
74
- const minutes = Number(tokens[index + 1]);
75
- if (Number.isFinite(minutes) && minutes >= 0) next.idleTimeoutMs = Math.round(minutes * 60_000);
76
- index += 2;
77
- continue;
78
- }
79
-
80
- if (token.startsWith("--idle-mins=")) {
81
- idleSpecified = true;
82
- const minutes = Number(token.slice(12));
83
- if (Number.isFinite(minutes) && minutes >= 0) next.idleTimeoutMs = Math.round(minutes * 60_000);
84
- index += 1;
85
- continue;
86
- }
87
-
88
- if (token === "--idle-secs" && tokens[index + 1] !== undefined) {
89
- idleSpecified = true;
90
- const seconds = Number(tokens[index + 1]);
91
- if (Number.isFinite(seconds) && seconds >= 0) next.idleTimeoutMs = Math.round(seconds * 1_000);
92
- index += 2;
93
- continue;
94
- }
95
-
96
- if (token.startsWith("--idle-secs=")) {
97
- idleSpecified = true;
98
- const seconds = Number(token.slice(12));
99
- if (Number.isFinite(seconds) && seconds >= 0) next.idleTimeoutMs = Math.round(seconds * 1_000);
100
- index += 1;
101
- continue;
102
- }
103
-
104
- if (/^\d+$/.test(token)) {
105
- next.port = Number(token);
106
- index += 1;
107
- continue;
108
- }
109
-
110
- if (!token.startsWith("--") && next.token === current.token) {
111
- tokenSpecified = true;
112
- next.token = token === "-" ? "" : token;
113
- index += 1;
114
- continue;
115
- }
116
-
117
- index += 1;
118
- }
119
-
120
- return { config: next, tokenSpecified, idleSpecified };
121
- }
@@ -1,250 +0,0 @@
1
- import { spawnSync } from "node:child_process";
2
- import { existsSync, readdirSync, statSync } from "node:fs";
3
- import { homedir } from "node:os";
4
- import { basename, dirname, join, resolve } from "node:path";
5
- import type { PhonePathSuggestion, PhonePathSuggestionMode } from "./types";
6
-
7
- const agentDirFromEnv = process.env.DM_CODING_AGENT_DIR?.trim() || process.env.PI_CODING_AGENT_DIR?.trim();
8
- const agentDir = agentDirFromEnv
9
- ? agentDirFromEnv
10
- : join(process.env.HOME || process.env.USERPROFILE || process.cwd(), ".dm", "agent");
11
-
12
- const PATH_SUGGESTION_LIMIT = 20;
13
- const PATH_SUGGESTION_MAX_RESULTS = 100;
14
-
15
- function resolveFdBinaryPath(): string | null {
16
- const bundledFd = join(agentDir, "bin", "fd");
17
- if (existsSync(bundledFd)) return bundledFd;
18
-
19
- for (const candidate of ["fd", "fdfind"]) {
20
- const result = spawnSync(candidate, ["--version"], { stdio: "ignore" });
21
- if (!result.error && result.status === 0) return candidate;
22
- }
23
-
24
- return null;
25
- }
26
-
27
- const fdBinaryPath = resolveFdBinaryPath();
28
-
29
- function stripWrappingQuotes(value: string): string {
30
- const trimmed = value.trim();
31
- if (
32
- (trimmed.startsWith('"') && trimmed.endsWith('"'))
33
- || (trimmed.startsWith("'") && trimmed.endsWith("'"))
34
- ) {
35
- return trimmed.slice(1, -1);
36
- }
37
- return trimmed;
38
- }
39
-
40
- function expandHomePath(value: string): string {
41
- if (value === "~") return homedir();
42
- if (value.startsWith("~/")) return join(homedir(), value.slice(2));
43
- return value;
44
- }
45
-
46
- export function resolvePhoneCdTargetPath(rawArgs: string | undefined, currentCwd: string, previousCwd?: string | null): string {
47
- const input = stripWrappingQuotes(rawArgs ?? "").trim();
48
-
49
- if (!input) return homedir();
50
- if (input === "-") {
51
- if (!previousCwd) {
52
- throw new Error("No previous directory available yet.");
53
- }
54
- return previousCwd;
55
- }
56
-
57
- const expanded = expandHomePath(input);
58
- return resolve(currentCwd, expanded);
59
- }
60
-
61
- function createCdPathSuggestions(prefix: string, currentCwd: string, previousCwd?: string | null): PhonePathSuggestion[] {
62
- const raw = prefix ?? "";
63
- const trimmed = raw.trimStart();
64
- const suggestions: PhonePathSuggestion[] = [];
65
-
66
- if ("-".startsWith(trimmed)) {
67
- suggestions.push({
68
- value: "-",
69
- label: "-",
70
- description: previousCwd || "Previous directory",
71
- isDirectory: true,
72
- kind: "previous",
73
- });
74
- }
75
-
76
- const expanded = expandHomePath(trimmed);
77
- const endsWithSeparator = /[\\/]$/.test(expanded);
78
- const resolvedInput = expanded ? resolve(currentCwd, expanded) : currentCwd;
79
-
80
- const baseDir = expanded
81
- ? endsWithSeparator
82
- ? resolvedInput
83
- : dirname(resolvedInput)
84
- : currentCwd;
85
-
86
- const partial = expanded && !endsWithSeparator ? basename(expanded) : "";
87
- const valuePrefix = trimmed
88
- ? endsWithSeparator
89
- ? trimmed
90
- : trimmed.slice(0, Math.max(0, trimmed.length - partial.length))
91
- : "";
92
-
93
- try {
94
- if (!existsSync(baseDir) || !statSync(baseDir).isDirectory()) {
95
- return suggestions.slice(0, PATH_SUGGESTION_LIMIT);
96
- }
97
-
98
- const directories = readdirSync(baseDir, { withFileTypes: true })
99
- .filter((entry) => {
100
- if (entry.isDirectory()) return true;
101
- if (!entry.isSymbolicLink()) return false;
102
- try {
103
- return statSync(join(baseDir, entry.name)).isDirectory();
104
- } catch {
105
- return false;
106
- }
107
- })
108
- .filter((entry) => !partial || entry.name.startsWith(partial))
109
- .sort((left, right) => left.name.localeCompare(right.name));
110
-
111
- for (const entry of directories.slice(0, PATH_SUGGESTION_LIMIT)) {
112
- suggestions.push({
113
- value: `${valuePrefix}${entry.name}/`,
114
- label: `${entry.name}/`,
115
- description: join(baseDir, entry.name),
116
- isDirectory: true,
117
- kind: "path",
118
- });
119
- }
120
- } catch {
121
- return suggestions.slice(0, PATH_SUGGESTION_LIMIT);
122
- }
123
-
124
- return suggestions.slice(0, PATH_SUGGESTION_LIMIT);
125
- }
126
-
127
- function resolveScopedMentionQuery(rawQuery: string, currentCwd: string): { baseDir: string; query: string; displayBase: string } | null {
128
- const slashIndex = rawQuery.lastIndexOf("/");
129
- if (slashIndex === -1) return null;
130
-
131
- const displayBase = rawQuery.slice(0, slashIndex + 1);
132
- const query = rawQuery.slice(slashIndex + 1);
133
- const baseDir = displayBase.startsWith("~/")
134
- ? expandHomePath(displayBase)
135
- : displayBase.startsWith("/")
136
- ? displayBase
137
- : join(currentCwd, displayBase);
138
-
139
- try {
140
- if (!statSync(baseDir).isDirectory()) return null;
141
- } catch {
142
- return null;
143
- }
144
-
145
- return { baseDir, query, displayBase };
146
- }
147
-
148
- function scopedPathForDisplay(displayBase: string, relativePath: string): string {
149
- if (displayBase === "/") return `/${relativePath}`;
150
- return `${displayBase}${relativePath}`;
151
- }
152
-
153
- function scorePhonePathEntry(filePath: string, query: string, isDirectory: boolean): number {
154
- if (!query) return isDirectory ? 2 : 1;
155
-
156
- const fileName = basename(filePath).toLowerCase();
157
- const normalizedQuery = query.toLowerCase();
158
- let score = 0;
159
-
160
- if (fileName === normalizedQuery) score = 100;
161
- else if (fileName.startsWith(normalizedQuery)) score = 80;
162
- else if (fileName.includes(normalizedQuery)) score = 50;
163
- else if (filePath.toLowerCase().includes(normalizedQuery)) score = 30;
164
-
165
- if (isDirectory && score > 0) score += 10;
166
- return score;
167
- }
168
-
169
- function walkDirectoryWithFd(baseDir: string, query: string, maxResults = PATH_SUGGESTION_MAX_RESULTS) {
170
- if (!fdBinaryPath) return [] as Array<{ path: string; isDirectory: boolean }>;
171
-
172
- const args = [
173
- "--base-directory",
174
- baseDir,
175
- "--max-results",
176
- String(maxResults),
177
- "--type",
178
- "f",
179
- "--type",
180
- "d",
181
- "--full-path",
182
- "--hidden",
183
- "--exclude",
184
- ".git",
185
- "--exclude",
186
- ".git/*",
187
- "--exclude",
188
- ".git/**",
189
- ];
190
-
191
- if (query) args.push(query);
192
-
193
- const result = spawnSync(fdBinaryPath, args, {
194
- encoding: "utf8",
195
- stdio: ["pipe", "pipe", "pipe"],
196
- maxBuffer: 10 * 1024 * 1024,
197
- });
198
-
199
- if (result.status !== 0 || !result.stdout) return [];
200
-
201
- return result.stdout
202
- .trim()
203
- .split("\n")
204
- .filter(Boolean)
205
- .map((line) => ({
206
- path: line.endsWith("/") ? line.slice(0, -1) : line,
207
- isDirectory: line.endsWith("/"),
208
- }))
209
- .filter((entry) => entry.path !== ".git" && !entry.path.startsWith(".git/") && !entry.path.includes("/.git/"));
210
- }
211
-
212
- function createMentionPathSuggestions(query: string, currentCwd: string): PhonePathSuggestion[] {
213
- const scopedQuery = resolveScopedMentionQuery(query, currentCwd);
214
- const fdBaseDir = scopedQuery?.baseDir ?? currentCwd;
215
- const fdQuery = scopedQuery?.query ?? query;
216
- const entries = walkDirectoryWithFd(fdBaseDir, fdQuery, PATH_SUGGESTION_MAX_RESULTS);
217
-
218
- return entries
219
- .map((entry) => ({
220
- ...entry,
221
- score: scorePhonePathEntry(entry.path, fdQuery, entry.isDirectory),
222
- }))
223
- .filter((entry) => entry.score > 0)
224
- .sort((left, right) => right.score - left.score)
225
- .slice(0, PATH_SUGGESTION_LIMIT)
226
- .map((entry) => {
227
- const displayPath = scopedQuery
228
- ? scopedPathForDisplay(scopedQuery.displayBase, entry.path)
229
- : entry.path;
230
- const completionPath = entry.isDirectory ? `${displayPath}/` : displayPath;
231
- return {
232
- value: completionPath,
233
- label: `${basename(entry.path)}${entry.isDirectory ? "/" : ""}`,
234
- description: displayPath,
235
- isDirectory: entry.isDirectory,
236
- kind: "path" as const,
237
- };
238
- });
239
- }
240
-
241
- export function listPhonePathSuggestions(
242
- mode: PhonePathSuggestionMode,
243
- query: string,
244
- currentCwd: string,
245
- previousCwd?: string | null,
246
- ): PhonePathSuggestion[] {
247
- return mode === "cd"
248
- ? createCdPathSuggestions(query, currentCwd, previousCwd)
249
- : createMentionPathSuggestions(query, currentCwd);
250
- }