@makefinks/daemon 0.1.3 → 0.2.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.
@@ -0,0 +1,250 @@
1
+ import { homedir } from "node:os";
2
+
3
+ const SENSITIVE_PATHS = [
4
+ "~/.ssh",
5
+ "~/.gnupg",
6
+ "~/.gpg",
7
+ "~/.aws",
8
+ "~/.azure",
9
+ "~/.config/gcloud",
10
+ "~/.kube",
11
+ "~/Library/Application Support/Google/Chrome",
12
+ "~/Library/Application Support/Firefox",
13
+ "~/Library/Application Support/Microsoft Edge",
14
+ "~/Library/Safari",
15
+ "~/.config/google-chrome",
16
+ "~/.config/chromium",
17
+ "~/.mozilla/firefox",
18
+ "~/Library/Keychains",
19
+ "~/.password-store",
20
+ "~/.local/share/keyrings",
21
+ "~/.env",
22
+ "~/.envrc",
23
+ "~/.netrc",
24
+ "~/Downloads",
25
+ "~/Documents",
26
+ "~/Desktop",
27
+ "~/Pictures",
28
+ "~/Movies",
29
+ "~/Music",
30
+ "~/Library/Messages",
31
+ "~/Library/Mail",
32
+ "~/Library/Calendars",
33
+ "~/Library/Contacts",
34
+ "~/Library/Cookies",
35
+ "~/.docker/config.json",
36
+ "~/.npmrc",
37
+ "~/.pypirc",
38
+ "~/.gem/credentials",
39
+ "~/.config/gh",
40
+ "~/.config/hub",
41
+ "~/.bash_history",
42
+ "~/.zsh_history",
43
+ "~/.node_repl_history",
44
+ "~/.python_history",
45
+ ];
46
+
47
+ const SENSITIVE_PATH_PATTERNS = [
48
+ /\bid_rsa\b/i,
49
+ /\bid_ed25519\b/i,
50
+ /\bid_ecdsa\b/i,
51
+ /\bid_dsa\b/i,
52
+ /\bauthorized_keys\b/i,
53
+ /\bknown_hosts\b/i,
54
+ /\.pem\b/i,
55
+ /\.key\b/i,
56
+ /private.*key/i,
57
+ /\.env(\.|$)/i,
58
+ /\.envrc\b/i,
59
+ /aws.*credentials/i,
60
+ /aws.*config/i,
61
+ /\bkeychain\b/i,
62
+ /\bkeyring\b/i,
63
+ /\bLogin Data\b/i,
64
+ /\bCookies\b/i,
65
+ /\bWeb Data\b/i,
66
+ /\bsecurity\s+(find|dump|export)/i,
67
+ ];
68
+
69
+ const DANGEROUS_COMMANDS = [
70
+ "rm",
71
+ "rmdir",
72
+ "mv",
73
+ "kill",
74
+ "killall",
75
+ "pkill",
76
+ "shutdown",
77
+ "reboot",
78
+ "halt",
79
+ "poweroff",
80
+ "init",
81
+ "systemctl",
82
+ "chmod",
83
+ "chown",
84
+ "chgrp",
85
+ "mkfs",
86
+ "fdisk",
87
+ "dd",
88
+ "format",
89
+ "sudo",
90
+ "su",
91
+ "doas",
92
+ "env",
93
+ "printenv",
94
+ "export",
95
+ "passwd",
96
+ "useradd",
97
+ "userdel",
98
+ "usermod",
99
+ "groupadd",
100
+ "groupdel",
101
+ "visudo",
102
+ "crontab",
103
+ "iptables",
104
+ "ufw",
105
+ "firewall-cmd",
106
+ "mount",
107
+ "umount",
108
+ "fstab",
109
+ "apt-get remove",
110
+ "apt-get purge",
111
+ "apt remove",
112
+ "apt purge",
113
+ "yum remove",
114
+ "yum erase",
115
+ "dnf remove",
116
+ "pacman -R",
117
+ "brew uninstall",
118
+ "npm uninstall -g",
119
+ "pip uninstall",
120
+ "truncate",
121
+ "shred",
122
+ "wipefs",
123
+ ">",
124
+ ">>",
125
+ "git push --force",
126
+ "git push -f",
127
+ "git reset --hard",
128
+ "git clean -fd",
129
+ "docker rm",
130
+ "docker rmi",
131
+ "docker system prune",
132
+ "kubectl delete",
133
+ "terraform destroy",
134
+ "drop database",
135
+ "drop table",
136
+ "delete from",
137
+ "truncate table",
138
+ ];
139
+
140
+ const DANGEROUS_PATTERNS = [
141
+ /\brm\s+(-[a-zA-Z]*r[a-zA-Z]*|-[a-zA-Z]*f[a-zA-Z]*|\s).*\//i,
142
+ /\brm\s+-rf?\s/i,
143
+ /\bkill\s+-9\b/i,
144
+ /\bsudo\s/i,
145
+ /\bsu\s+-?\s*$/i,
146
+ /\bchmod\s+[0-7]{3,4}\s/i,
147
+ /\bchown\s/i,
148
+ /\bdd\s+if=/i,
149
+ />\s*\/dev\//i,
150
+ /\|.*\bsh\b/i,
151
+ /\|.*\bbash\b/i,
152
+ /curl.*\|\s*(ba)?sh/i,
153
+ /wget.*\|\s*(ba)?sh/i,
154
+ /eval\s*\$/i,
155
+ /\$\(.*\)/,
156
+ /`.*`/,
157
+ /\benv\s*$/i,
158
+ /\bprintenv\s*$/i,
159
+ /\bexport\s+-p/i,
160
+ /\bset\s*\|/i,
161
+ /echo\s+\$\w*_?(KEY|TOKEN|SECRET|PASSWORD|CREDENTIALS)/i,
162
+ ];
163
+
164
+ function expandPath(path: string): string {
165
+ if (path.startsWith("~/")) {
166
+ return path.replace("~", homedir());
167
+ }
168
+ if (path === "~") {
169
+ return homedir();
170
+ }
171
+ return path;
172
+ }
173
+
174
+ function isSensitivePathAccess(command: string): boolean {
175
+ const normalizedCmd = command.trim();
176
+ const home = homedir();
177
+
178
+ for (const sensitivePath of SENSITIVE_PATHS) {
179
+ const expandedPath = expandPath(sensitivePath);
180
+ if (normalizedCmd.includes(expandedPath)) {
181
+ return true;
182
+ }
183
+ if (sensitivePath.startsWith("~/") && normalizedCmd.includes(sensitivePath)) {
184
+ return true;
185
+ }
186
+ if (normalizedCmd.includes(sensitivePath.replace("~", "$HOME"))) {
187
+ return true;
188
+ }
189
+ }
190
+
191
+ for (const pattern of SENSITIVE_PATH_PATTERNS) {
192
+ if (pattern.test(normalizedCmd)) {
193
+ return true;
194
+ }
195
+ }
196
+
197
+ const homeAccessPattern = new RegExp(
198
+ `(cat|less|head|tail|more|bat|grep|rg|awk|sed|find|ls|tree|du)\\s+[^|;]*?(~(?:/[^\\s/]+)?(?:\\s|$)|${home.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(?:/[^\\s/]+)?(?:\\s|$))`
199
+ );
200
+ if (homeAccessPattern.test(normalizedCmd)) {
201
+ const allowedHomePaths = [
202
+ "~/projects",
203
+ "~/code",
204
+ "~/dev",
205
+ "~/src",
206
+ "~/repos",
207
+ "~/workspace",
208
+ "~/work",
209
+ "~/.local/bin",
210
+ "~/go",
211
+ "~/bin",
212
+ ];
213
+ const isAllowedPath = allowedHomePaths.some((allowed) => {
214
+ const expanded = expandPath(allowed);
215
+ return normalizedCmd.includes(expanded) || normalizedCmd.includes(allowed);
216
+ });
217
+ if (!isAllowedPath) {
218
+ return true;
219
+ }
220
+ }
221
+
222
+ return false;
223
+ }
224
+
225
+ function isDangerousCommand(command: string): boolean {
226
+ const normalizedCmd = command.toLowerCase().trim();
227
+
228
+ for (const dangerous of DANGEROUS_COMMANDS) {
229
+ if (dangerous.includes(" ")) {
230
+ if (normalizedCmd.includes(dangerous.toLowerCase())) {
231
+ return true;
232
+ }
233
+ } else {
234
+ const wordBoundaryRegex = new RegExp(`\\b${dangerous}\\b`, "i");
235
+ if (wordBoundaryRegex.test(command)) {
236
+ return true;
237
+ }
238
+ }
239
+ }
240
+
241
+ for (const pattern of DANGEROUS_PATTERNS) {
242
+ if (pattern.test(command)) {
243
+ return true;
244
+ }
245
+ }
246
+
247
+ return false;
248
+ }
249
+
250
+ export { isDangerousCommand, isSensitivePathAccess };
@@ -29,6 +29,8 @@ export interface MenuState {
29
29
  setShowHotkeysPane: React.Dispatch<React.SetStateAction<boolean>>;
30
30
  showGroundingMenu: boolean;
31
31
  setShowGroundingMenu: React.Dispatch<React.SetStateAction<boolean>>;
32
+ showUrlMenu: boolean;
33
+ setShowUrlMenu: React.Dispatch<React.SetStateAction<boolean>>;
32
34
  }
33
35
 
34
36
  export interface DeviceState {
@@ -131,6 +131,10 @@ function parseSessionUsage(raw: string): TokenUsage {
131
131
  subagentPromptTokens: typeof parsed.subagentPromptTokens === "number" ? parsed.subagentPromptTokens : 0,
132
132
  subagentCompletionTokens:
133
133
  typeof parsed.subagentCompletionTokens === "number" ? parsed.subagentCompletionTokens : 0,
134
+ latestTurnPromptTokens:
135
+ typeof parsed.latestTurnPromptTokens === "number" ? parsed.latestTurnPromptTokens : undefined,
136
+ latestTurnCompletionTokens:
137
+ typeof parsed.latestTurnCompletionTokens === "number" ? parsed.latestTurnCompletionTokens : undefined,
134
138
  };
135
139
  } catch {
136
140
  return { ...DEFAULT_SESSION_USAGE };
@@ -42,6 +42,10 @@ export interface TokenUsage {
42
42
  subagentTotalTokens?: number;
43
43
  subagentPromptTokens?: number;
44
44
  subagentCompletionTokens?: number;
45
+ /** Latest turn's prompt tokens (for context window % calculation) */
46
+ latestTurnPromptTokens?: number;
47
+ /** Latest turn's completion tokens (for context window % calculation) */
48
+ latestTurnCompletionTokens?: number;
45
49
  }
46
50
 
47
51
  /**
@@ -403,3 +407,13 @@ export interface GroundingMap {
403
407
  createdAt: string;
404
408
  items: GroundedStatement[];
405
409
  }
410
+
411
+ export interface UrlMenuItem {
412
+ url: string;
413
+ groundedCount: number;
414
+ readPercent?: number;
415
+ highlightsCount?: number;
416
+ status: "ok" | "error";
417
+ error?: string;
418
+ lastSeenIndex: number;
419
+ }
@@ -0,0 +1,155 @@
1
+ import type { ContentBlock, ConversationMessage, GroundingMap, UrlMenuItem } from "../types";
2
+
3
+ function normalizeUrlKey(rawUrl: string): string {
4
+ try {
5
+ const parsed = new URL(rawUrl);
6
+ parsed.hash = "";
7
+ return parsed.toString();
8
+ } catch {
9
+ return rawUrl;
10
+ }
11
+ }
12
+
13
+ function computeCoveragePercent(intervals: Array<[number, number]>, totalLines: number): number | undefined {
14
+ if (!Number.isFinite(totalLines) || totalLines <= 0) return undefined;
15
+ if (intervals.length === 0) return undefined;
16
+
17
+ const sorted = [...intervals].sort((a, b) => a[0] - b[0]);
18
+ let covered = 0;
19
+ let curStart = sorted[0]?.[0] ?? 0;
20
+ let curEnd = sorted[0]?.[1] ?? 0;
21
+
22
+ for (const [start, end] of sorted.slice(1)) {
23
+ if (start <= curEnd) {
24
+ curEnd = Math.max(curEnd, end);
25
+ continue;
26
+ }
27
+ covered += Math.max(0, curEnd - curStart);
28
+ curStart = start;
29
+ curEnd = end;
30
+ }
31
+ covered += Math.max(0, curEnd - curStart);
32
+
33
+ const percent = Math.round((covered / totalLines) * 100);
34
+ return Math.max(0, Math.min(100, percent));
35
+ }
36
+
37
+ export function deriveUrlMenuItems(params: {
38
+ conversationHistory: ConversationMessage[];
39
+ currentContentBlocks: ContentBlock[];
40
+ latestGroundingMap: GroundingMap | null;
41
+ }): UrlMenuItem[] {
42
+ const { conversationHistory, currentContentBlocks, latestGroundingMap } = params;
43
+
44
+ const intervalsByUrl = new Map<string, Array<[number, number]>>();
45
+ const totalLinesByUrl = new Map<string, number>();
46
+ const highlightsCountByUrl = new Map<string, number>();
47
+ const lastSeenIndexByUrl = new Map<string, number>();
48
+ const statusByUrl = new Map<string, "ok" | "error">();
49
+ const errorByUrl = new Map<string, string>();
50
+
51
+ const allBlocks = [
52
+ ...conversationHistory.flatMap((msg) => msg.contentBlocks ?? []),
53
+ ...currentContentBlocks,
54
+ ];
55
+
56
+ for (const [blockIndex, block] of allBlocks.entries()) {
57
+ if (block.type !== "tool") continue;
58
+ if (block.call.name !== "fetchUrls" && block.call.name !== "renderUrl") continue;
59
+
60
+ const input = block.call.input as { url?: string } | undefined;
61
+ const url = input?.url;
62
+ if (!url) continue;
63
+
64
+ lastSeenIndexByUrl.set(url, blockIndex);
65
+
66
+ const result = block.result as
67
+ | {
68
+ lineOffset?: number;
69
+ lineLimit?: number;
70
+ totalLines?: number;
71
+ highlights?: unknown[];
72
+ success?: boolean;
73
+ error?: string;
74
+ }
75
+ | undefined;
76
+
77
+ if (!result || typeof result !== "object") continue;
78
+
79
+ if (result.success === false && typeof result.error === "string" && result.error.trim().length > 0) {
80
+ statusByUrl.set(url, "error");
81
+ errorByUrl.set(url, result.error.trim());
82
+ } else if (result.success === true) {
83
+ statusByUrl.set(url, "ok");
84
+ }
85
+
86
+ if (Array.isArray(result.highlights)) {
87
+ highlightsCountByUrl.set(url, result.highlights.length);
88
+ }
89
+
90
+ if (
91
+ typeof result.totalLines === "number" &&
92
+ Number.isFinite(result.totalLines) &&
93
+ result.totalLines > 0
94
+ ) {
95
+ const prev = totalLinesByUrl.get(url) ?? 0;
96
+ totalLinesByUrl.set(url, Math.max(prev, result.totalLines));
97
+ }
98
+
99
+ if (
100
+ typeof result.lineOffset === "number" &&
101
+ typeof result.lineLimit === "number" &&
102
+ Number.isFinite(result.lineOffset) &&
103
+ Number.isFinite(result.lineLimit) &&
104
+ result.lineLimit > 0 &&
105
+ result.lineOffset >= 0
106
+ ) {
107
+ const start = result.lineOffset;
108
+ const end = result.lineOffset + result.lineLimit;
109
+ const list = intervalsByUrl.get(url) ?? [];
110
+ list.push([start, end]);
111
+ intervalsByUrl.set(url, list);
112
+ }
113
+ }
114
+
115
+ const groundedCountByUrl = new Map<string, number>();
116
+ for (const groundedItem of latestGroundingMap?.items ?? []) {
117
+ const groundedUrl = groundedItem.source?.url;
118
+ if (!groundedUrl) continue;
119
+ const next = (groundedCountByUrl.get(groundedUrl) ?? 0) + 1;
120
+ groundedCountByUrl.set(groundedUrl, next);
121
+ }
122
+
123
+ function lookupGroundedCount(url: string): number {
124
+ const direct = groundedCountByUrl.get(url);
125
+ if (direct !== undefined) return direct;
126
+
127
+ const key = normalizeUrlKey(url);
128
+ for (const [gUrl, count] of groundedCountByUrl.entries()) {
129
+ if (normalizeUrlKey(gUrl) === key) return count;
130
+ }
131
+ return 0;
132
+ }
133
+
134
+ const urls = [...lastSeenIndexByUrl.keys()];
135
+ return urls.map((url) => {
136
+ const groundedCount = lookupGroundedCount(url);
137
+ const highlightsCount = highlightsCountByUrl.get(url);
138
+ const totalLines = totalLinesByUrl.get(url);
139
+ const intervals = intervalsByUrl.get(url) ?? [];
140
+ const readPercent = totalLines !== undefined ? computeCoveragePercent(intervals, totalLines) : undefined;
141
+ const error = errorByUrl.get(url);
142
+ const status = statusByUrl.get(url) ?? (error ? "error" : "ok");
143
+ const lastSeenIndex = lastSeenIndexByUrl.get(url) ?? 0;
144
+
145
+ return {
146
+ url,
147
+ groundedCount,
148
+ readPercent,
149
+ highlightsCount,
150
+ status,
151
+ error,
152
+ lastSeenIndex,
153
+ };
154
+ });
155
+ }
@@ -209,14 +209,8 @@ export function isTodoInput(input: unknown): input is TodoInput {
209
209
  );
210
210
  }
211
211
 
212
- /**
213
- * Format token count with K suffix for thousands
214
- */
215
212
  export function formatTokenCount(count: number): string {
216
- // if (count >= 1000) {
217
- // return `${(count / 1000).toFixed(1)}k`;
218
- // }
219
- return String(count);
213
+ return count.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ".");
220
214
  }
221
215
 
222
216
  export function formatContextWindowK(contextLength: number): string {