@bahulam/code 0.1.5 → 0.1.7

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 (179) hide show
  1. package/LICENSE +1 -1
  2. package/NOTICE +6 -6
  3. package/README.md +10 -17
  4. package/package.json +9 -9
  5. package/src/agents/loader.mjs +1 -1
  6. package/src/agents/workflow_loader.mjs +2 -2
  7. package/src/auth/tarang-auth.mjs +3 -2
  8. package/src/config/model-catalog-default.json +4 -3
  9. package/src/config/model-catalog.mjs +2 -0
  10. package/src/context/prose-chunker.mjs +162 -5
  11. package/src/core/attachments.mjs +65 -3
  12. package/src/core/bundled-runtime.mjs +1 -1
  13. package/src/core/headless.mjs +14 -0
  14. package/src/core/jsonl-writer.mjs +3 -3
  15. package/src/core/lint-resolver.mjs +472 -0
  16. package/src/core/local-agent.mjs +15 -4
  17. package/src/core/local-store.mjs +4 -0
  18. package/src/core/output-filter.mjs +21 -25
  19. package/src/core/pricing.mjs +10 -3
  20. package/src/core/stream-client.mjs +112 -16
  21. package/src/core/tool-executor.mjs +123 -31
  22. package/src/daemon/http-dashboard.mjs +400 -0
  23. package/src/daemon/session-core.mjs +1 -0
  24. package/src/local-service/agent-relay.mjs +868 -0
  25. package/src/local-service/approval-bridge.mjs +222 -0
  26. package/src/local-service/browser.mjs +24 -0
  27. package/src/local-service/command.mjs +155 -0
  28. package/src/local-service/file-access.mjs +393 -0
  29. package/src/local-service/machine.mjs +86 -0
  30. package/src/local-service/paths.mjs +29 -0
  31. package/src/local-service/preview-converters.mjs +260 -0
  32. package/src/local-service/server.mjs +2644 -0
  33. package/src/local-service/session-store.mjs +221 -0
  34. package/src/onboarding/preflight.mjs +1 -1
  35. package/src/terminal/analytics.mjs +2 -6
  36. package/src/terminal/ansi.mjs +11 -3
  37. package/src/terminal/main.mjs +37 -11
  38. package/src/terminal/model-catalog-display.mjs +111 -0
  39. package/src/terminal/repl-format.mjs +4 -1
  40. package/src/terminal/repl-model-form.mjs +81 -14
  41. package/src/terminal/repl-render.mjs +35 -5
  42. package/src/terminal/repl-state.mjs +12 -0
  43. package/src/terminal/repl.mjs +255 -55
  44. package/src/terminal/tool-display.mjs +32 -0
  45. package/src/terminal/watch-state.mjs +118 -0
  46. package/src/tools/analyze-image.mjs +158 -0
  47. package/src/tools/bash.mjs +11 -9
  48. package/src/tools/generate-image.mjs +411 -0
  49. package/src/tools/lint.mjs +24 -43
  50. package/src/tools/project-overview.mjs +5 -2
  51. package/src/tools/registry.mjs +4 -0
  52. package/src/ui/icons.mjs +2 -0
  53. package/src/ui/palette.mjs +1 -1
  54. package/src/ui/slash-commands.mjs +4 -6
  55. package/src/ui/tool-card.mjs +7 -3
  56. package/pulse/app/activity/page.tsx +0 -190
  57. package/pulse/app/api/activity/route.ts +0 -138
  58. package/pulse/app/api/benchmark/route.ts +0 -113
  59. package/pulse/app/api/benchmarks/route.ts +0 -195
  60. package/pulse/app/api/costs/route.ts +0 -88
  61. package/pulse/app/api/export/route.ts +0 -77
  62. package/pulse/app/api/history/route.ts +0 -11
  63. package/pulse/app/api/import/route.ts +0 -31
  64. package/pulse/app/api/memory/route.ts +0 -50
  65. package/pulse/app/api/plans/route.ts +0 -9
  66. package/pulse/app/api/projects/[slug]/route.ts +0 -96
  67. package/pulse/app/api/projects/route.ts +0 -121
  68. package/pulse/app/api/sessions/[id]/replay/route.ts +0 -20
  69. package/pulse/app/api/sessions/[id]/route.ts +0 -31
  70. package/pulse/app/api/sessions/route.ts +0 -112
  71. package/pulse/app/api/settings/route.ts +0 -14
  72. package/pulse/app/api/stats/route.ts +0 -143
  73. package/pulse/app/api/todos/route.ts +0 -9
  74. package/pulse/app/api/tools/route.ts +0 -160
  75. package/pulse/app/benchmarks/page.tsx +0 -224
  76. package/pulse/app/costs/page.tsx +0 -179
  77. package/pulse/app/export/page.tsx +0 -465
  78. package/pulse/app/favicon.ico +0 -0
  79. package/pulse/app/globals.css +0 -263
  80. package/pulse/app/help/page.tsx +0 -143
  81. package/pulse/app/history/page.tsx +0 -157
  82. package/pulse/app/layout.tsx +0 -46
  83. package/pulse/app/memory/page.tsx +0 -365
  84. package/pulse/app/overview-client.tsx +0 -393
  85. package/pulse/app/page.tsx +0 -14
  86. package/pulse/app/plans/page.tsx +0 -308
  87. package/pulse/app/projects/[slug]/page.tsx +0 -390
  88. package/pulse/app/projects/page.tsx +0 -110
  89. package/pulse/app/sessions/[id]/page.tsx +0 -243
  90. package/pulse/app/sessions/page.tsx +0 -39
  91. package/pulse/app/settings/page.tsx +0 -188
  92. package/pulse/app/todos/page.tsx +0 -211
  93. package/pulse/app/tools/page.tsx +0 -249
  94. package/pulse/cli.js +0 -164
  95. package/pulse/components/activity/day-of-week-chart.tsx +0 -35
  96. package/pulse/components/activity/streak-card.tsx +0 -36
  97. package/pulse/components/costs/cache-efficiency-panel.tsx +0 -76
  98. package/pulse/components/costs/cost-by-project-chart.tsx +0 -48
  99. package/pulse/components/costs/cost-over-time-chart.tsx +0 -95
  100. package/pulse/components/costs/model-token-table.tsx +0 -60
  101. package/pulse/components/global-search.tsx +0 -193
  102. package/pulse/components/keyboard-nav-provider.tsx +0 -23
  103. package/pulse/components/layout/bottom-nav.tsx +0 -53
  104. package/pulse/components/layout/client-layout.tsx +0 -31
  105. package/pulse/components/layout/sidebar-context.tsx +0 -50
  106. package/pulse/components/layout/sidebar.tsx +0 -183
  107. package/pulse/components/layout/top-bar.tsx +0 -121
  108. package/pulse/components/overview/activity-heatmap.tsx +0 -107
  109. package/pulse/components/overview/conversation-table.tsx +0 -148
  110. package/pulse/components/overview/model-breakdown-donut.tsx +0 -95
  111. package/pulse/components/overview/peak-hours-chart.tsx +0 -87
  112. package/pulse/components/overview/project-activity-donut.tsx +0 -96
  113. package/pulse/components/overview/stat-card.tsx +0 -102
  114. package/pulse/components/overview/usage-over-time-chart.tsx +0 -166
  115. package/pulse/components/projects/project-card.tsx +0 -175
  116. package/pulse/components/sessions/replay/assistant-markdown.tsx +0 -94
  117. package/pulse/components/sessions/replay/compaction-card.tsx +0 -25
  118. package/pulse/components/sessions/replay/session-sidebar.tsx +0 -231
  119. package/pulse/components/sessions/replay/token-accumulation-chart.tsx +0 -98
  120. package/pulse/components/sessions/replay/tool-call-badge.tsx +0 -127
  121. package/pulse/components/sessions/replay/turn-cards.tsx +0 -220
  122. package/pulse/components/sessions/replay/user-tool-result.tsx +0 -158
  123. package/pulse/components/sessions/session-badges.tsx +0 -49
  124. package/pulse/components/sessions/session-table.tsx +0 -299
  125. package/pulse/components/theme-provider.tsx +0 -44
  126. package/pulse/components/tools/feature-adoption-table.tsx +0 -58
  127. package/pulse/components/tools/mcp-server-panel.tsx +0 -45
  128. package/pulse/components/tools/tool-ranking-chart.tsx +0 -57
  129. package/pulse/components/tools/version-history-table.tsx +0 -32
  130. package/pulse/components/ui/alert.tsx +0 -66
  131. package/pulse/components/ui/badge.tsx +0 -48
  132. package/pulse/components/ui/breadcrumb.tsx +0 -109
  133. package/pulse/components/ui/button.tsx +0 -64
  134. package/pulse/components/ui/calendar.tsx +0 -220
  135. package/pulse/components/ui/card.tsx +0 -92
  136. package/pulse/components/ui/command.tsx +0 -158
  137. package/pulse/components/ui/dialog.tsx +0 -158
  138. package/pulse/components/ui/input.tsx +0 -21
  139. package/pulse/components/ui/popover.tsx +0 -89
  140. package/pulse/components/ui/progress.tsx +0 -31
  141. package/pulse/components/ui/select.tsx +0 -190
  142. package/pulse/components/ui/separator.tsx +0 -28
  143. package/pulse/components/ui/sheet.tsx +0 -143
  144. package/pulse/components/ui/skeleton.tsx +0 -13
  145. package/pulse/components/ui/table.tsx +0 -116
  146. package/pulse/components/ui/tabs.tsx +0 -91
  147. package/pulse/components/ui/tooltip.tsx +0 -57
  148. package/pulse/components/use-global-keyboard-nav.ts +0 -79
  149. package/pulse/components.json +0 -23
  150. package/pulse/eslint.config.mjs +0 -18
  151. package/pulse/lib/bahulam-paths.ts +0 -23
  152. package/pulse/lib/claude-reader.ts +0 -592
  153. package/pulse/lib/decode.ts +0 -129
  154. package/pulse/lib/pricing.ts +0 -102
  155. package/pulse/lib/replay-parser.ts +0 -165
  156. package/pulse/lib/tool-categories.ts +0 -140
  157. package/pulse/lib/utils.ts +0 -6
  158. package/pulse/next-env.d.ts +0 -6
  159. package/pulse/next.config.ts +0 -16
  160. package/pulse/package.json +0 -45
  161. package/pulse/postcss.config.mjs +0 -7
  162. package/pulse/public/activity.png +0 -0
  163. package/pulse/public/cc-lens.png +0 -0
  164. package/pulse/public/command-k.png +0 -0
  165. package/pulse/public/costs.png +0 -0
  166. package/pulse/public/dashboard-dark.png +0 -0
  167. package/pulse/public/dashboard-white.png +0 -0
  168. package/pulse/public/export.png +0 -0
  169. package/pulse/public/file.svg +0 -1
  170. package/pulse/public/globe.svg +0 -1
  171. package/pulse/public/next.svg +0 -1
  172. package/pulse/public/projects.png +0 -0
  173. package/pulse/public/session-chat.png +0 -0
  174. package/pulse/public/todos.png +0 -0
  175. package/pulse/public/tools.png +0 -0
  176. package/pulse/public/vercel.svg +0 -1
  177. package/pulse/public/window.svg +0 -1
  178. package/pulse/tsconfig.json +0 -34
  179. package/pulse/types/claude.ts +0 -294
@@ -0,0 +1,393 @@
1
+ /**
2
+ * Workspace-bounded file access for the local service.
3
+ */
4
+
5
+ import * as fs from 'node:fs';
6
+ import * as path from 'node:path';
7
+
8
+ const DEFAULT_MAX_ENTRIES = 300;
9
+ const DEFAULT_MAX_TEXT_BYTES = 256 * 1024;
10
+ export const DEFAULT_MAX_RAW_BYTES = 100 * 1024 * 1024;
11
+
12
+ const EXACT_FILE_TYPES = new Map([
13
+ ['dockerfile', { kind: 'code', language: 'dockerfile', label: 'Dockerfile', textLike: true }],
14
+ ['containerfile', { kind: 'code', language: 'dockerfile', label: 'Containerfile', textLike: true }],
15
+ ['makefile', { kind: 'code', language: 'makefile', label: 'Makefile', textLike: true }],
16
+ ['rakefile', { kind: 'code', language: 'ruby', label: 'Ruby', textLike: true }],
17
+ ['gemfile', { kind: 'code', language: 'ruby', label: 'Ruby', textLike: true }],
18
+ ['pipfile', { kind: 'config', language: 'toml', label: 'TOML', textLike: true }],
19
+ ['requirements.txt', { kind: 'config', language: 'plaintext', label: 'Requirements', textLike: true }],
20
+ ['package.json', { kind: 'config', language: 'json', label: 'JSON', textLike: true }],
21
+ ['tsconfig.json', { kind: 'config', language: 'json', label: 'JSON', textLike: true }],
22
+ ['.gitignore', { kind: 'config', language: 'plaintext', label: 'Git Ignore', textLike: true }],
23
+ ['.dockerignore', { kind: 'config', language: 'plaintext', label: 'Docker Ignore', textLike: true }],
24
+ ]);
25
+
26
+ const EXTENSION_FILE_TYPES = new Map([
27
+ ['txt', { kind: 'text', language: 'plaintext', label: 'Text', textLike: true }],
28
+ ['log', { kind: 'text', language: 'plaintext', label: 'Log', textLike: true }],
29
+ ['md', { kind: 'markdown', language: 'markdown', label: 'Markdown', textLike: true }],
30
+ ['mdx', { kind: 'markdown', language: 'markdown', label: 'MDX', textLike: true }],
31
+ ['mmd', { kind: 'mermaid', language: 'markdown', label: 'Mermaid', textLike: true }],
32
+ ['mermaid', { kind: 'mermaid', language: 'markdown', label: 'Mermaid', textLike: true }],
33
+ ['drawio', { kind: 'drawio', language: 'xml', label: 'Draw.io', textLike: true }],
34
+ ['dio', { kind: 'drawio', language: 'xml', label: 'Draw.io', textLike: true }],
35
+ ['json', { kind: 'config', language: 'json', label: 'JSON', textLike: true }],
36
+ ['jsonl', { kind: 'config', language: 'json', label: 'JSONL', textLike: true }],
37
+ ['ipynb', { kind: 'notebook', language: 'json', label: 'Jupyter Notebook', textLike: true }],
38
+ ['yaml', { kind: 'config', language: 'yaml', label: 'YAML', textLike: true }],
39
+ ['yml', { kind: 'config', language: 'yaml', label: 'YAML', textLike: true }],
40
+ ['toml', { kind: 'config', language: 'toml', label: 'TOML', textLike: true }],
41
+ ['ini', { kind: 'config', language: 'ini', label: 'INI', textLike: true }],
42
+ ['env', { kind: 'config', language: 'plaintext', label: 'Environment', textLike: true }],
43
+ ['properties', { kind: 'config', language: 'plaintext', label: 'Properties', textLike: true }],
44
+ ['xml', { kind: 'code', language: 'xml', label: 'XML', textLike: true }],
45
+ ['html', { kind: 'code', language: 'html', label: 'HTML', textLike: true }],
46
+ ['htm', { kind: 'code', language: 'html', label: 'HTML', textLike: true }],
47
+ ['css', { kind: 'code', language: 'css', label: 'CSS', textLike: true }],
48
+ ['scss', { kind: 'code', language: 'scss', label: 'SCSS', textLike: true }],
49
+ ['sass', { kind: 'code', language: 'scss', label: 'Sass', textLike: true }],
50
+ ['less', { kind: 'code', language: 'less', label: 'Less', textLike: true }],
51
+ ['js', { kind: 'code', language: 'javascript', label: 'JavaScript', textLike: true }],
52
+ ['mjs', { kind: 'code', language: 'javascript', label: 'JavaScript', textLike: true }],
53
+ ['cjs', { kind: 'code', language: 'javascript', label: 'JavaScript', textLike: true }],
54
+ ['jsx', { kind: 'code', language: 'javascript', label: 'JavaScript JSX', textLike: true }],
55
+ ['ts', { kind: 'code', language: 'typescript', label: 'TypeScript', textLike: true }],
56
+ ['tsx', { kind: 'code', language: 'typescript', label: 'TypeScript JSX', textLike: true }],
57
+ ['vue', { kind: 'code', language: 'html', label: 'Vue', textLike: true }],
58
+ ['svelte', { kind: 'code', language: 'html', label: 'Svelte', textLike: true }],
59
+ ['py', { kind: 'code', language: 'python', label: 'Python', textLike: true }],
60
+ ['rb', { kind: 'code', language: 'ruby', label: 'Ruby', textLike: true }],
61
+ ['go', { kind: 'code', language: 'go', label: 'Go', textLike: true }],
62
+ ['rs', { kind: 'code', language: 'rust', label: 'Rust', textLike: true }],
63
+ ['java', { kind: 'code', language: 'java', label: 'Java', textLike: true }],
64
+ ['kt', { kind: 'code', language: 'kotlin', label: 'Kotlin', textLike: true }],
65
+ ['kts', { kind: 'code', language: 'kotlin', label: 'Kotlin', textLike: true }],
66
+ ['swift', { kind: 'code', language: 'swift', label: 'Swift', textLike: true }],
67
+ ['c', { kind: 'code', language: 'c', label: 'C', textLike: true }],
68
+ ['h', { kind: 'code', language: 'c', label: 'C Header', textLike: true }],
69
+ ['cpp', { kind: 'code', language: 'cpp', label: 'C++', textLike: true }],
70
+ ['cc', { kind: 'code', language: 'cpp', label: 'C++', textLike: true }],
71
+ ['cxx', { kind: 'code', language: 'cpp', label: 'C++', textLike: true }],
72
+ ['hpp', { kind: 'code', language: 'cpp', label: 'C++ Header', textLike: true }],
73
+ ['cs', { kind: 'code', language: 'csharp', label: 'C#', textLike: true }],
74
+ ['php', { kind: 'code', language: 'php', label: 'PHP', textLike: true }],
75
+ ['dart', { kind: 'code', language: 'dart', label: 'Dart', textLike: true }],
76
+ ['scala', { kind: 'code', language: 'scala', label: 'Scala', textLike: true }],
77
+ ['r', { kind: 'code', language: 'r', label: 'R', textLike: true }],
78
+ ['lua', { kind: 'code', language: 'lua', label: 'Lua', textLike: true }],
79
+ ['pl', { kind: 'code', language: 'perl', label: 'Perl', textLike: true }],
80
+ ['ex', { kind: 'code', language: 'elixir', label: 'Elixir', textLike: true }],
81
+ ['exs', { kind: 'code', language: 'elixir', label: 'Elixir', textLike: true }],
82
+ ['clj', { kind: 'code', language: 'clojure', label: 'Clojure', textLike: true }],
83
+ ['fs', { kind: 'code', language: 'fsharp', label: 'F#', textLike: true }],
84
+ ['jl', { kind: 'code', language: 'julia', label: 'Julia', textLike: true }],
85
+ ['zig', { kind: 'code', language: 'plaintext', label: 'Zig', textLike: true }],
86
+ ['sh', { kind: 'code', language: 'shell', label: 'Shell', textLike: true }],
87
+ ['bash', { kind: 'code', language: 'shell', label: 'Bash', textLike: true }],
88
+ ['zsh', { kind: 'code', language: 'shell', label: 'Zsh', textLike: true }],
89
+ ['fish', { kind: 'code', language: 'shell', label: 'Fish', textLike: true }],
90
+ ['ps1', { kind: 'code', language: 'powershell', label: 'PowerShell', textLike: true }],
91
+ ['bat', { kind: 'code', language: 'bat', label: 'Batch', textLike: true }],
92
+ ['cmd', { kind: 'code', language: 'bat', label: 'Batch', textLike: true }],
93
+ ['sql', { kind: 'code', language: 'sql', label: 'SQL', textLike: true }],
94
+ ['graphql', { kind: 'code', language: 'graphql', label: 'GraphQL', textLike: true }],
95
+ ['gql', { kind: 'code', language: 'graphql', label: 'GraphQL', textLike: true }],
96
+ ['proto', { kind: 'code', language: 'protobuf', label: 'Protocol Buffers', textLike: true }],
97
+ ['tf', { kind: 'code', language: 'hcl', label: 'Terraform', textLike: true }],
98
+ ['hcl', { kind: 'code', language: 'hcl', label: 'HCL', textLike: true }],
99
+ ['dax', { kind: 'code', language: 'msdax', label: 'DAX', textLike: true }],
100
+ ['pq', { kind: 'code', language: 'powerquery', label: 'Power Query', textLike: true }],
101
+ ['m', { kind: 'code', language: 'powerquery', label: 'Power Query', textLike: true }],
102
+ ['lookml', { kind: 'code', language: 'plaintext', label: 'LookML', textLike: true }],
103
+ ['csv', { kind: 'table', language: 'csv', label: 'CSV', textLike: true }],
104
+ ['tsv', { kind: 'table', language: 'plaintext', label: 'TSV', textLike: true }],
105
+ ['png', { kind: 'image', language: null, label: 'PNG Image', textLike: false }],
106
+ ['jpg', { kind: 'image', language: null, label: 'JPEG Image', textLike: false }],
107
+ ['jpeg', { kind: 'image', language: null, label: 'JPEG Image', textLike: false }],
108
+ ['gif', { kind: 'image', language: null, label: 'GIF Image', textLike: false }],
109
+ ['webp', { kind: 'image', language: null, label: 'WebP Image', textLike: false }],
110
+ ['svg', { kind: 'image', language: 'xml', label: 'SVG Image', textLike: false }],
111
+ ['bmp', { kind: 'image', language: null, label: 'Bitmap Image', textLike: false }],
112
+ ['ico', { kind: 'image', language: null, label: 'Icon', textLike: false }],
113
+ ['pdf', { kind: 'pdf', language: null, label: 'PDF', textLike: false }],
114
+ ['xlsx', { kind: 'spreadsheet', language: null, label: 'Excel Workbook', textLike: false }],
115
+ ['xls', { kind: 'spreadsheet', language: null, label: 'Excel Workbook', textLike: false }],
116
+ ['ods', { kind: 'spreadsheet', language: null, label: 'Spreadsheet', textLike: false }],
117
+ ['docx', { kind: 'document', language: null, label: 'Word Document', textLike: false }],
118
+ ['doc', { kind: 'document', language: null, label: 'Word Document', textLike: false }],
119
+ ['odt', { kind: 'document', language: null, label: 'Document', textLike: false }],
120
+ ['pptx', { kind: 'presentation', language: null, label: 'PowerPoint', textLike: false }],
121
+ ['ppt', { kind: 'presentation', language: null, label: 'PowerPoint', textLike: false }],
122
+ ['odp', { kind: 'presentation', language: null, label: 'Presentation', textLike: false }],
123
+ ]);
124
+
125
+ export function resolveWorkspacePath(session, requestedPath = '.') {
126
+ if (!session?.root_path) throw new Error('session.root_path is required');
127
+ const root = fs.realpathSync(session.root_path);
128
+ let rel = String(requestedPath || '.').trim();
129
+ if (!rel || rel === '/') rel = '.';
130
+ rel = rel.replace(/^[/\\]+/, '');
131
+
132
+ const candidate = path.resolve(root, rel);
133
+ let canonical;
134
+ try {
135
+ canonical = fs.realpathSync(candidate);
136
+ } catch {
137
+ canonical = realpathForMissing(candidate);
138
+ }
139
+
140
+ if (!isWithin(root, canonical)) {
141
+ const err = new Error('Path is outside the granted workspace root');
142
+ err.code = 'OUTSIDE_WORKSPACE';
143
+ throw err;
144
+ }
145
+
146
+ return canonical;
147
+ }
148
+
149
+ export function listWorkspacePath(session, requestedPath = '.', { maxEntries = DEFAULT_MAX_ENTRIES } = {}) {
150
+ const target = resolveWorkspacePath(session, requestedPath);
151
+ const stat = fs.statSync(target);
152
+ const root = fs.realpathSync(session.root_path);
153
+
154
+ if (stat.isDirectory()) {
155
+ const entries = fs.readdirSync(target, { withFileTypes: true })
156
+ .filter((entry) => !shouldHideEntry(entry.name))
157
+ .sort((a, b) => {
158
+ if (a.isDirectory() !== b.isDirectory()) return a.isDirectory() ? -1 : 1;
159
+ return a.name.localeCompare(b.name);
160
+ })
161
+ .slice(0, maxEntries)
162
+ .map((entry) => {
163
+ const fullPath = path.join(target, entry.name);
164
+ let childStat = null;
165
+ try {
166
+ childStat = fs.statSync(fullPath);
167
+ } catch {}
168
+ return {
169
+ name: entry.name,
170
+ path: normalizeRelative(root, fullPath),
171
+ type: entry.isDirectory() ? 'directory' : entry.isFile() ? 'file' : 'other',
172
+ size: childStat?.size ?? null,
173
+ updated_at: childStat?.mtime ? childStat.mtime.toISOString() : null,
174
+ };
175
+ });
176
+
177
+ return {
178
+ ok: true,
179
+ type: 'directory',
180
+ path: normalizeRelative(root, target),
181
+ entries,
182
+ truncated: entries.length >= maxEntries,
183
+ };
184
+ }
185
+
186
+ if (stat.isFile()) {
187
+ return {
188
+ ok: true,
189
+ type: 'file',
190
+ path: normalizeRelative(root, target),
191
+ file: describeFile(root, target, stat),
192
+ preview: readTextPreview(target),
193
+ };
194
+ }
195
+
196
+ return {
197
+ ok: false,
198
+ type: 'other',
199
+ path: normalizeRelative(root, target),
200
+ error: 'Unsupported filesystem entry',
201
+ };
202
+ }
203
+
204
+ export function readWorkspaceFile(session, requestedPath, { maxBytes = DEFAULT_MAX_TEXT_BYTES } = {}) {
205
+ const target = resolveWorkspacePath(session, requestedPath);
206
+ const stat = fs.statSync(target);
207
+ if (!stat.isFile()) {
208
+ const err = new Error('Requested path is not a file');
209
+ err.code = 'NOT_FILE';
210
+ throw err;
211
+ }
212
+ if (stat.size > maxBytes) {
213
+ const err = new Error(`File is larger than ${maxBytes} bytes`);
214
+ err.code = 'FILE_TOO_LARGE';
215
+ throw err;
216
+ }
217
+ return fs.readFileSync(target);
218
+ }
219
+
220
+ export function describeWorkspaceFileName(filePath) {
221
+ const name = path.basename(String(filePath || ''));
222
+ const lowerName = name.toLowerCase();
223
+ const ext = path.extname(name).slice(1).toLowerCase();
224
+ const exact = EXACT_FILE_TYPES.get(lowerName);
225
+ if (exact) return fileTypeResult(ext, exact);
226
+ const byExtension = EXTENSION_FILE_TYPES.get(ext);
227
+ if (byExtension) return fileTypeResult(ext, byExtension);
228
+ if (!ext) {
229
+ return fileTypeResult('', {
230
+ kind: 'text',
231
+ language: 'plaintext',
232
+ label: 'Text',
233
+ textLike: true,
234
+ });
235
+ }
236
+ return fileTypeResult(ext, {
237
+ kind: 'binary',
238
+ language: null,
239
+ label: `${ext.toUpperCase()} File`,
240
+ textLike: false,
241
+ });
242
+ }
243
+
244
+ export function contentTypeForPath(filePath) {
245
+ const ext = path.extname(String(filePath || '')).slice(1).toLowerCase();
246
+ switch (ext) {
247
+ case 'png': return 'image/png';
248
+ case 'jpg':
249
+ case 'jpeg': return 'image/jpeg';
250
+ case 'gif': return 'image/gif';
251
+ case 'webp': return 'image/webp';
252
+ case 'svg': return 'image/svg+xml';
253
+ case 'bmp': return 'image/bmp';
254
+ case 'ico': return 'image/x-icon';
255
+ case 'pdf': return 'application/pdf';
256
+ case 'txt':
257
+ case 'log':
258
+ case 'md':
259
+ case 'mdx':
260
+ case 'mmd':
261
+ case 'mermaid':
262
+ case 'drawio':
263
+ case 'dio':
264
+ case 'json':
265
+ case 'jsonl':
266
+ case 'ipynb':
267
+ case 'yaml':
268
+ case 'yml':
269
+ case 'toml':
270
+ case 'ini':
271
+ case 'csv':
272
+ case 'tsv':
273
+ case 'xml':
274
+ case 'html':
275
+ case 'htm':
276
+ case 'css':
277
+ case 'js':
278
+ case 'mjs':
279
+ case 'cjs':
280
+ case 'ts':
281
+ case 'tsx':
282
+ case 'jsx':
283
+ case 'py':
284
+ case 'rb':
285
+ case 'go':
286
+ case 'rs':
287
+ case 'java':
288
+ case 'sh':
289
+ case 'sql':
290
+ return 'text/plain; charset=utf-8';
291
+ default:
292
+ return 'application/octet-stream';
293
+ }
294
+ }
295
+
296
+ function describeFile(root, target, stat) {
297
+ const type = describeWorkspaceFileName(target);
298
+ return {
299
+ name: path.basename(target),
300
+ path: normalizeRelative(root, target),
301
+ size: stat.size,
302
+ updated_at: stat.mtime.toISOString(),
303
+ extension: path.extname(target).slice(1).toLowerCase(),
304
+ text_like: type.textLike,
305
+ kind: type.kind,
306
+ viewer: type.viewer,
307
+ language: type.language,
308
+ label: type.label,
309
+ };
310
+ }
311
+
312
+ function readTextPreview(target) {
313
+ if (!isTextLike(target)) return null;
314
+ try {
315
+ const fd = fs.openSync(target, 'r');
316
+ try {
317
+ const buf = Buffer.alloc(DEFAULT_MAX_TEXT_BYTES);
318
+ const bytesRead = fs.readSync(fd, buf, 0, buf.length, 0);
319
+ const content = buf.subarray(0, bytesRead).toString('utf-8');
320
+ if (content.includes('\u0000')) return null;
321
+ return {
322
+ content,
323
+ truncated: fs.statSync(target).size > bytesRead,
324
+ };
325
+ } finally {
326
+ fs.closeSync(fd);
327
+ }
328
+ } catch {
329
+ return null;
330
+ }
331
+ }
332
+
333
+ function isTextLike(target) {
334
+ return describeWorkspaceFileName(target).textLike;
335
+ }
336
+
337
+ function fileTypeResult(extension, info) {
338
+ return {
339
+ extension,
340
+ kind: info.kind,
341
+ viewer: viewerForKind(info.kind),
342
+ language: info.language,
343
+ label: info.label,
344
+ textLike: Boolean(info.textLike),
345
+ };
346
+ }
347
+
348
+ function viewerForKind(kind) {
349
+ switch (kind) {
350
+ case 'markdown': return 'markdown';
351
+ case 'mermaid': return 'mermaid';
352
+ case 'drawio': return 'drawio';
353
+ case 'image': return 'image';
354
+ case 'pdf': return 'pdf';
355
+ case 'spreadsheet': return 'spreadsheet';
356
+ case 'document': return 'document';
357
+ case 'presentation': return 'presentation';
358
+ case 'table': return 'table';
359
+ case 'notebook': return 'notebook';
360
+ case 'code':
361
+ case 'config':
362
+ case 'text':
363
+ return 'code';
364
+ default:
365
+ return 'unsupported';
366
+ }
367
+ }
368
+
369
+ function shouldHideEntry(name) {
370
+ return ['node_modules', '.git', '.next', 'dist', 'build', '__pycache__'].includes(name);
371
+ }
372
+
373
+ function normalizeRelative(root, target) {
374
+ const rel = path.relative(root, target);
375
+ return rel ? rel.split(path.sep).join('/') : '.';
376
+ }
377
+
378
+ function isWithin(root, candidate) {
379
+ const rel = path.relative(root, candidate);
380
+ return rel === '' || (!rel.startsWith('..') && !path.isAbsolute(rel));
381
+ }
382
+
383
+ function realpathForMissing(candidate) {
384
+ let current = candidate;
385
+ const missing = [];
386
+ while (!fs.existsSync(current)) {
387
+ const parent = path.dirname(current);
388
+ if (parent === current) break;
389
+ missing.unshift(path.basename(current));
390
+ current = parent;
391
+ }
392
+ return path.join(fs.realpathSync(current), ...missing);
393
+ }
@@ -0,0 +1,86 @@
1
+ /**
2
+ * Local machine identity and loopback enforcement.
3
+ *
4
+ * The local service is meant for browser + CLI on the same machine. It never
5
+ * binds to a LAN interface, and every request is checked for loopback origin.
6
+ */
7
+
8
+ import * as crypto from 'node:crypto';
9
+ import * as fs from 'node:fs';
10
+ import * as os from 'node:os';
11
+ import * as path from 'node:path';
12
+ import { localServiceRoot } from './paths.mjs';
13
+
14
+ const MACHINE_FILE = 'machine.json';
15
+
16
+ export function getLocalMachineIdentity() {
17
+ const file = path.join(localServiceRoot(), MACHINE_FILE);
18
+ try {
19
+ const existing = JSON.parse(fs.readFileSync(file, 'utf-8'));
20
+ if (existing?.id) return existing;
21
+ } catch {}
22
+
23
+ const identity = {
24
+ id: `mach_${crypto.randomBytes(12).toString('hex')}`,
25
+ hostname: os.hostname(),
26
+ platform: process.platform,
27
+ arch: process.arch,
28
+ created_at: new Date().toISOString(),
29
+ };
30
+ fs.mkdirSync(localServiceRoot(), { recursive: true, mode: 0o700 });
31
+ fs.writeFileSync(file, `${JSON.stringify(identity, null, 2)}\n`, { mode: 0o600 });
32
+ return identity;
33
+ }
34
+
35
+ export function normalizeLoopbackHost(host = '127.0.0.1') {
36
+ const value = String(host || '127.0.0.1').trim().toLowerCase();
37
+ if (value === '127.0.0.1' || value === 'localhost') return '127.0.0.1';
38
+ if (value === '::1' || value === '[::1]') return '::1';
39
+ const err = new Error('Local service can only bind to loopback hosts: 127.0.0.1, localhost, or ::1');
40
+ err.code = 'LOCAL_ONLY';
41
+ throw err;
42
+ }
43
+
44
+ export function loopbackHostForUrl(host) {
45
+ return host === '::1' ? '[::1]' : host;
46
+ }
47
+
48
+ export function assertLoopbackRequest(req) {
49
+ const remoteAddress = req.socket?.remoteAddress || '';
50
+ const hostHeader = parseHostHeader(req.headers.host || '');
51
+
52
+ if (!isLoopbackAddress(remoteAddress)) {
53
+ const err = new Error(`Rejected non-local request from ${remoteAddress || 'unknown address'}`);
54
+ err.code = 'LOCAL_ONLY';
55
+ throw err;
56
+ }
57
+
58
+ if (hostHeader && !isLoopbackHostHeader(hostHeader)) {
59
+ const err = new Error(`Rejected non-local Host header: ${hostHeader}`);
60
+ err.code = 'LOCAL_ONLY';
61
+ throw err;
62
+ }
63
+ }
64
+
65
+ export function isLoopbackAddress(address) {
66
+ const value = String(address || '').trim().toLowerCase();
67
+ if (value === 'localhost' || value === '::1') return true;
68
+ if (value.startsWith('127.')) return true;
69
+ if (value.startsWith('::ffff:127.')) return true;
70
+ return false;
71
+ }
72
+
73
+ function isLoopbackHostHeader(host) {
74
+ const value = String(host || '').trim().toLowerCase();
75
+ return value === 'localhost' || value === '127.0.0.1' || value === '::1' || value === '[::1]';
76
+ }
77
+
78
+ function parseHostHeader(hostHeader) {
79
+ const value = String(hostHeader || '').trim();
80
+ if (!value) return '';
81
+ if (value.startsWith('[')) {
82
+ const end = value.indexOf(']');
83
+ return end === -1 ? value.toLowerCase() : value.slice(0, end + 1).toLowerCase();
84
+ }
85
+ return value.split(':')[0].toLowerCase();
86
+ }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Local service paths.
3
+ *
4
+ * The local service is a separate product/runtime surface from the terminal
5
+ * daemon. It owns browser-based local workspaces and document processing.
6
+ */
7
+
8
+ import * as path from 'node:path';
9
+ import { bahulamHome } from '../core/paths.mjs';
10
+
11
+ export function localServiceRoot() {
12
+ return path.join(bahulamHome(), 'local-service');
13
+ }
14
+
15
+ export function localServiceSessionsRoot() {
16
+ return path.join(localServiceRoot(), 'sessions');
17
+ }
18
+
19
+ export function localServiceSessionDir(sessionId) {
20
+ return path.join(localServiceSessionsRoot(), sessionId);
21
+ }
22
+
23
+ export function localServiceSessionPath(sessionId) {
24
+ return path.join(localServiceSessionDir(sessionId), 'session.json');
25
+ }
26
+
27
+ export function localServiceStatePath() {
28
+ return path.join(localServiceRoot(), 'state.json');
29
+ }