smolerclaw 1.0.0 → 1.0.2

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 (72) hide show
  1. package/dist/README.md +159 -0
  2. package/package.json +11 -3
  3. package/.github/workflows/ci.yml +0 -30
  4. package/.github/workflows/release.yml +0 -67
  5. package/bun.lock +0 -33
  6. package/install.ps1 +0 -119
  7. package/skills/business.md +0 -77
  8. package/skills/default.md +0 -77
  9. package/src/ansi.ts +0 -164
  10. package/src/approval.ts +0 -74
  11. package/src/auth.ts +0 -125
  12. package/src/briefing.ts +0 -52
  13. package/src/claude.ts +0 -267
  14. package/src/cli.ts +0 -137
  15. package/src/clipboard.ts +0 -27
  16. package/src/config.ts +0 -87
  17. package/src/context-window.ts +0 -190
  18. package/src/context.ts +0 -125
  19. package/src/decisions.ts +0 -122
  20. package/src/email.ts +0 -123
  21. package/src/errors.ts +0 -78
  22. package/src/export.ts +0 -82
  23. package/src/finance.ts +0 -148
  24. package/src/git.ts +0 -62
  25. package/src/history.ts +0 -100
  26. package/src/images.ts +0 -68
  27. package/src/index.ts +0 -1431
  28. package/src/investigate.ts +0 -415
  29. package/src/markdown.ts +0 -125
  30. package/src/memos.ts +0 -191
  31. package/src/models.ts +0 -94
  32. package/src/monitor.ts +0 -169
  33. package/src/morning.ts +0 -108
  34. package/src/news.ts +0 -329
  35. package/src/openai-provider.ts +0 -127
  36. package/src/people.ts +0 -472
  37. package/src/personas.ts +0 -99
  38. package/src/platform.ts +0 -84
  39. package/src/plugins.ts +0 -125
  40. package/src/pomodoro.ts +0 -169
  41. package/src/providers.ts +0 -70
  42. package/src/retry.ts +0 -108
  43. package/src/session.ts +0 -128
  44. package/src/skills.ts +0 -102
  45. package/src/tasks.ts +0 -418
  46. package/src/tokens.ts +0 -102
  47. package/src/tool-safety.ts +0 -100
  48. package/src/tools.ts +0 -1479
  49. package/src/tui.ts +0 -693
  50. package/src/types.ts +0 -55
  51. package/src/undo.ts +0 -83
  52. package/src/windows.ts +0 -299
  53. package/src/workflows.ts +0 -197
  54. package/tests/ansi.test.ts +0 -58
  55. package/tests/approval.test.ts +0 -43
  56. package/tests/briefing.test.ts +0 -10
  57. package/tests/cli.test.ts +0 -53
  58. package/tests/context-window.test.ts +0 -83
  59. package/tests/images.test.ts +0 -28
  60. package/tests/memos.test.ts +0 -116
  61. package/tests/models.test.ts +0 -34
  62. package/tests/news.test.ts +0 -13
  63. package/tests/path-guard.test.ts +0 -37
  64. package/tests/people.test.ts +0 -204
  65. package/tests/skills.test.ts +0 -35
  66. package/tests/ssrf.test.ts +0 -80
  67. package/tests/tasks.test.ts +0 -152
  68. package/tests/tokens.test.ts +0 -44
  69. package/tests/tool-safety.test.ts +0 -55
  70. package/tests/windows-security.test.ts +0 -59
  71. package/tests/windows.test.ts +0 -20
  72. package/tsconfig.json +0 -19
package/src/people.ts DELETED
@@ -1,472 +0,0 @@
1
- /**
2
- * People management — personal CRM for team, family, and contacts.
3
- * Stores people, interaction logs, delegated tasks, and follow-up reminders.
4
- */
5
-
6
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
7
- import { join } from 'node:path'
8
-
9
- // ─── Types ──────────────────────────────────────────────────
10
-
11
- export type PersonGroup = 'equipe' | 'familia' | 'contato'
12
-
13
- export interface Person {
14
- id: string
15
- name: string
16
- group: PersonGroup
17
- role?: string // "dev frontend", "esposa", "filho", "fornecedor"
18
- contact?: string // phone, email, etc.
19
- notes?: string // free-text notes
20
- createdAt: string
21
- }
22
-
23
- export interface Interaction {
24
- id: string
25
- personId: string
26
- date: string // ISO date
27
- type: InteractionType
28
- summary: string
29
- followUpDate?: string // ISO date — when to follow up
30
- followUpDone: boolean
31
- }
32
-
33
- export type InteractionType = 'conversa' | 'email' | 'reuniao' | 'ligacao' | 'mensagem' | 'delegacao' | 'entrega' | 'outro'
34
-
35
- export interface Delegation {
36
- id: string
37
- personId: string
38
- task: string
39
- assignedAt: string
40
- dueDate?: string // ISO date
41
- status: 'pendente' | 'em_andamento' | 'concluido' | 'atrasado'
42
- notes?: string
43
- }
44
-
45
- interface PeopleData {
46
- people: Person[]
47
- interactions: Interaction[]
48
- delegations: Delegation[]
49
- }
50
-
51
- // ─── Storage ────────────────────────────────────────────────
52
-
53
- let _dataDir = ''
54
- let _data: PeopleData = { people: [], interactions: [], delegations: [] }
55
-
56
- const DATA_FILE = () => join(_dataDir, 'people.json')
57
-
58
- function save(): void {
59
- writeFileSync(DATA_FILE(), JSON.stringify(_data, null, 2))
60
- }
61
-
62
- function load(): void {
63
- const file = DATA_FILE()
64
- if (!existsSync(file)) {
65
- _data = { people: [], interactions: [], delegations: [] }
66
- return
67
- }
68
- try {
69
- const raw = JSON.parse(readFileSync(file, 'utf-8'))
70
- _data = {
71
- people: raw.people || [],
72
- interactions: raw.interactions || [],
73
- delegations: raw.delegations || [],
74
- }
75
- } catch {
76
- _data = { people: [], interactions: [], delegations: [] }
77
- }
78
- }
79
-
80
- // ─── Init ───────────────────────────────────────────────────
81
-
82
- export function initPeople(dataDir: string): void {
83
- _dataDir = dataDir
84
- if (!existsSync(dataDir)) mkdirSync(dataDir, { recursive: true })
85
- load()
86
- }
87
-
88
- // ─── People CRUD ────────────────────────────────────────────
89
-
90
- export function addPerson(name: string, group: PersonGroup, role?: string, contact?: string): Person {
91
- const person: Person = {
92
- id: genId(),
93
- name: name.trim(),
94
- group,
95
- role: role?.trim(),
96
- contact: contact?.trim(),
97
- createdAt: new Date().toISOString(),
98
- }
99
- _data = { ..._data, people: [..._data.people, person] }
100
- save()
101
- return person
102
- }
103
-
104
- export function updatePerson(idOrName: string, updates: Partial<Pick<Person, 'name' | 'group' | 'role' | 'contact' | 'notes'>>): Person | null {
105
- const person = findPerson(idOrName)
106
- if (!person) return null
107
-
108
- _data = {
109
- ..._data,
110
- people: _data.people.map((p) =>
111
- p.id === person.id ? { ...p, ...stripUndefined(updates) } : p,
112
- ),
113
- }
114
- save()
115
- return _data.people.find((p) => p.id === person.id) || null
116
- }
117
-
118
- export function removePerson(idOrName: string): boolean {
119
- const person = findPerson(idOrName)
120
- if (!person) return false
121
-
122
- _data = {
123
- ..._data,
124
- people: _data.people.filter((p) => p.id !== person.id),
125
- interactions: _data.interactions.filter((i) => i.personId !== person.id),
126
- delegations: _data.delegations.filter((d) => d.personId !== person.id),
127
- }
128
- save()
129
- return true
130
- }
131
-
132
- export function findPerson(idOrName: string): Person | null {
133
- const lower = idOrName.toLowerCase()
134
- return _data.people.find(
135
- (p) => p.id === idOrName || p.name.toLowerCase().includes(lower),
136
- ) || null
137
- }
138
-
139
- export function listPeople(group?: PersonGroup): Person[] {
140
- if (group) return _data.people.filter((p) => p.group === group)
141
- return [..._data.people]
142
- }
143
-
144
- // ─── Interactions ───────────────────────────────────────────
145
-
146
- export function logInteraction(
147
- personIdOrName: string,
148
- type: InteractionType,
149
- summary: string,
150
- followUpDate?: Date,
151
- ): Interaction | null {
152
- const person = findPerson(personIdOrName)
153
- if (!person) return null
154
-
155
- const interaction: Interaction = {
156
- id: genId(),
157
- personId: person.id,
158
- date: new Date().toISOString(),
159
- type,
160
- summary: summary.trim(),
161
- followUpDate: followUpDate?.toISOString(),
162
- followUpDone: false,
163
- }
164
- _data = { ..._data, interactions: [..._data.interactions, interaction] }
165
- save()
166
- return interaction
167
- }
168
-
169
- export function getInteractions(personIdOrName: string, limit = 10): Interaction[] {
170
- const person = findPerson(personIdOrName)
171
- if (!person) return []
172
-
173
- return _data.interactions
174
- .filter((i) => i.personId === person.id)
175
- .sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime())
176
- .slice(0, limit)
177
- }
178
-
179
- export function getPendingFollowUps(): Array<{ person: Person; interaction: Interaction }> {
180
- const now = new Date()
181
- const results: Array<{ person: Person; interaction: Interaction }> = []
182
-
183
- for (const interaction of _data.interactions) {
184
- if (interaction.followUpDone || !interaction.followUpDate) continue
185
- const followUp = new Date(interaction.followUpDate)
186
- if (isNaN(followUp.getTime())) continue
187
- if (followUp <= now) {
188
- const person = _data.people.find((p) => p.id === interaction.personId)
189
- if (person) results.push({ person, interaction })
190
- }
191
- }
192
-
193
- return results.sort((a, b) =>
194
- new Date(a.interaction.followUpDate!).getTime() - new Date(b.interaction.followUpDate!).getTime(),
195
- )
196
- }
197
-
198
- export function markFollowUpDone(interactionId: string): boolean {
199
- const found = _data.interactions.find((i) => i.id === interactionId)
200
- if (!found) return false
201
-
202
- _data = {
203
- ..._data,
204
- interactions: _data.interactions.map((i) =>
205
- i.id === interactionId ? { ...i, followUpDone: true } : i,
206
- ),
207
- }
208
- save()
209
- return true
210
- }
211
-
212
- // ─── Delegations ────────────────────────────────────────────
213
-
214
- export function delegateTask(
215
- personIdOrName: string,
216
- task: string,
217
- dueDate?: Date,
218
- ): Delegation | null {
219
- const person = findPerson(personIdOrName)
220
- if (!person) return null
221
-
222
- const delegation: Delegation = {
223
- id: genId(),
224
- personId: person.id,
225
- task: task.trim(),
226
- assignedAt: new Date().toISOString(),
227
- dueDate: dueDate?.toISOString(),
228
- status: 'pendente',
229
- }
230
- _data = { ..._data, delegations: [..._data.delegations, delegation] }
231
- save()
232
- return delegation
233
- }
234
-
235
- export function updateDelegation(
236
- delegationId: string,
237
- status: Delegation['status'],
238
- notes?: string,
239
- ): Delegation | null {
240
- const found = _data.delegations.find((d) => d.id === delegationId)
241
- if (!found) return null
242
-
243
- _data = {
244
- ..._data,
245
- delegations: _data.delegations.map((d) =>
246
- d.id === delegationId ? { ...d, status, notes: notes || d.notes } : d,
247
- ),
248
- }
249
- save()
250
- return _data.delegations.find((d) => d.id === delegationId) || null
251
- }
252
-
253
- export function getDelegations(personIdOrName?: string, onlyPending = true): Delegation[] {
254
- let results = [..._data.delegations]
255
-
256
- if (personIdOrName) {
257
- const person = findPerson(personIdOrName)
258
- if (!person) return []
259
- results = results.filter((d) => d.personId === person.id)
260
- }
261
-
262
- if (onlyPending) {
263
- results = results.filter((d) => d.status !== 'concluido')
264
- }
265
-
266
- // Mark overdue delegations
267
- const now = new Date()
268
- results = results.map((d) => {
269
- if (d.status === 'pendente' && d.dueDate) {
270
- const due = new Date(d.dueDate)
271
- if (!isNaN(due.getTime()) && due < now) {
272
- return { ...d, status: 'atrasado' as const }
273
- }
274
- }
275
- return d
276
- })
277
-
278
- return results.sort((a, b) => {
279
- // Overdue first, then by due date
280
- if (a.status === 'atrasado' && b.status !== 'atrasado') return -1
281
- if (b.status === 'atrasado' && a.status !== 'atrasado') return 1
282
- const da = a.dueDate ? new Date(a.dueDate).getTime() : Infinity
283
- const db = b.dueDate ? new Date(b.dueDate).getTime() : Infinity
284
- return da - db
285
- })
286
- }
287
-
288
- // ─── Formatting ─────────────────────────────────────────────
289
-
290
- const GROUP_LABELS: Record<PersonGroup, string> = {
291
- equipe: 'Equipe',
292
- familia: 'Familia',
293
- contato: 'Contato',
294
- }
295
-
296
- export function formatPeopleList(people: Person[]): string {
297
- if (people.length === 0) return 'Nenhuma pessoa cadastrada.'
298
-
299
- const grouped = new Map<PersonGroup, Person[]>()
300
- for (const p of people) {
301
- const list = grouped.get(p.group) || []
302
- grouped.set(p.group, [...list, p])
303
- }
304
-
305
- const sections: string[] = []
306
- const order: PersonGroup[] = ['equipe', 'familia', 'contato']
307
-
308
- for (const group of order) {
309
- const groupPeople = grouped.get(group)
310
- if (!groupPeople?.length) continue
311
-
312
- const lines = groupPeople.map((p) => {
313
- const role = p.role ? ` (${p.role})` : ''
314
- const contact = p.contact ? ` — ${p.contact}` : ''
315
- return ` ${p.name}${role}${contact} [${p.id}]`
316
- })
317
- sections.push(`--- ${GROUP_LABELS[group]} ---\n${lines.join('\n')}`)
318
- }
319
-
320
- return sections.join('\n\n')
321
- }
322
-
323
- export function formatPersonDetail(person: Person): string {
324
- const lines: string[] = []
325
- lines.push(`${person.name} [${person.id}]`)
326
- lines.push(`Grupo: ${GROUP_LABELS[person.group]}`)
327
- if (person.role) lines.push(`Papel: ${person.role}`)
328
- if (person.contact) lines.push(`Contato: ${person.contact}`)
329
- if (person.notes) lines.push(`Notas: ${person.notes}`)
330
-
331
- // Recent interactions
332
- const interactions = getInteractions(person.id, 5)
333
- if (interactions.length > 0) {
334
- lines.push('\nInteracoes recentes:')
335
- for (const i of interactions) {
336
- const date = new Date(i.date).toLocaleDateString('pt-BR', { day: '2-digit', month: '2-digit' })
337
- lines.push(` [${date}] ${i.type}: ${i.summary}`)
338
- }
339
- }
340
-
341
- // Pending delegations
342
- const delegations = getDelegations(person.id)
343
- if (delegations.length > 0) {
344
- lines.push('\nTarefas delegadas:')
345
- for (const d of delegations) {
346
- const statusIcon = d.status === 'atrasado' ? '!!' : d.status === 'em_andamento' ? '>>' : ' '
347
- const due = d.dueDate ? ` (${formatDate(d.dueDate)})` : ''
348
- lines.push(` ${statusIcon} ${d.task}${due} [${d.status}]`)
349
- }
350
- }
351
-
352
- return lines.join('\n')
353
- }
354
-
355
- export function formatDelegationList(delegations: Delegation[]): string {
356
- if (delegations.length === 0) return 'Nenhuma tarefa delegada pendente.'
357
-
358
- const lines = delegations.map((d) => {
359
- const person = _data.people.find((p) => p.id === d.personId)
360
- const name = person?.name || '?'
361
- const statusIcon = d.status === 'atrasado' ? '!! ' : d.status === 'em_andamento' ? '>> ' : ' '
362
- const due = d.dueDate ? ` (${formatDate(d.dueDate)})` : ''
363
- return `${statusIcon}${name}: ${d.task}${due} [${d.status}] [${d.id}]`
364
- })
365
-
366
- return `Delegacoes (${delegations.length}):\n${lines.join('\n')}`
367
- }
368
-
369
- export function formatFollowUps(items: Array<{ person: Person; interaction: Interaction }>): string {
370
- if (items.length === 0) return 'Nenhum follow-up pendente.'
371
-
372
- const lines = items.map(({ person, interaction }) => {
373
- const date = formatDate(interaction.followUpDate!)
374
- return ` [${date}] ${person.name}: ${interaction.summary} [${interaction.id}]`
375
- })
376
-
377
- return `Follow-ups pendentes (${items.length}):\n${lines.join('\n')}`
378
- }
379
-
380
- // ─── Dashboard ──────────────────────────────────────────────
381
-
382
- export function generatePeopleDashboard(): string {
383
- const sections: string[] = []
384
- sections.push('=== PAINEL DE PESSOAS ===\n')
385
-
386
- // Summary counts
387
- const teamCount = _data.people.filter((p) => p.group === 'equipe').length
388
- const familyCount = _data.people.filter((p) => p.group === 'familia').length
389
- const contactCount = _data.people.filter((p) => p.group === 'contato').length
390
- sections.push(`Equipe: ${teamCount} | Familia: ${familyCount} | Contatos: ${contactCount}`)
391
-
392
- // Overdue follow-ups
393
- const followUps = getPendingFollowUps()
394
- if (followUps.length > 0) {
395
- sections.push(`\n!! ${followUps.length} follow-up(s) pendente(s):`)
396
- for (const { person, interaction } of followUps.slice(0, 5)) {
397
- sections.push(` ${person.name}: ${interaction.summary}`)
398
- }
399
- }
400
-
401
- // Overdue/pending delegations
402
- const allDelegations = getDelegations()
403
- const overdue = allDelegations.filter((d) => d.status === 'atrasado')
404
- const pending = allDelegations.filter((d) => d.status === 'pendente' || d.status === 'em_andamento')
405
-
406
- if (overdue.length > 0) {
407
- sections.push(`\n!! ${overdue.length} delegacao(oes) atrasada(s):`)
408
- for (const d of overdue.slice(0, 5)) {
409
- const person = _data.people.find((p) => p.id === d.personId)
410
- sections.push(` ${person?.name}: ${d.task}`)
411
- }
412
- }
413
-
414
- if (pending.length > 0) {
415
- sections.push(`\n${pending.length} delegacao(oes) em andamento`)
416
- }
417
-
418
- // Recent interactions (last 3)
419
- const recent = [..._data.interactions]
420
- .sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime())
421
- .slice(0, 3)
422
-
423
- if (recent.length > 0) {
424
- sections.push('\nUltimas interacoes:')
425
- for (const i of recent) {
426
- const person = _data.people.find((p) => p.id === i.personId)
427
- const date = new Date(i.date).toLocaleDateString('pt-BR', { day: '2-digit', month: '2-digit' })
428
- sections.push(` [${date}] ${person?.name}: ${i.summary}`)
429
- }
430
- }
431
-
432
- sections.push('\n========================')
433
- return sections.join('\n')
434
- }
435
-
436
- // ─── Helpers ────────────────────────────────────────────────
437
-
438
- function genId(): string {
439
- const chars = 'abcdefghijklmnopqrstuvwxyz0123456789'
440
- let id = ''
441
- for (let i = 0; i < 6; i++) {
442
- id += chars[Math.floor(Math.random() * chars.length)]
443
- }
444
- return id
445
- }
446
-
447
- function formatDate(isoDate: string): string {
448
- const d = new Date(isoDate)
449
- if (isNaN(d.getTime())) return '?'
450
- const now = new Date()
451
- const today = new Date(now.getFullYear(), now.getMonth(), now.getDate())
452
- const target = new Date(d.getFullYear(), d.getMonth(), d.getDate())
453
-
454
- if (target.getTime() === today.getTime()) return 'hoje'
455
- const tomorrow = new Date(today)
456
- tomorrow.setDate(tomorrow.getDate() + 1)
457
- if (target.getTime() === tomorrow.getTime()) return 'amanha'
458
-
459
- const diff = Math.floor((target.getTime() - today.getTime()) / 86_400_000)
460
- if (diff < 0) return `${Math.abs(diff)}d atras`
461
- if (diff <= 7) return `em ${diff}d`
462
-
463
- return d.toLocaleDateString('pt-BR', { day: '2-digit', month: '2-digit' })
464
- }
465
-
466
- function stripUndefined(obj: Record<string, unknown>): Record<string, unknown> {
467
- const result: Record<string, unknown> = {}
468
- for (const [key, val] of Object.entries(obj)) {
469
- if (val !== undefined) result[key] = val
470
- }
471
- return result
472
- }
package/src/personas.ts DELETED
@@ -1,99 +0,0 @@
1
- /**
2
- * Prompt personas — switchable assistant modes.
3
- */
4
-
5
- export interface Persona {
6
- name: string
7
- description: string
8
- systemPrompt: string
9
- }
10
-
11
- export const PERSONAS: Record<string, Persona> = {
12
- default: {
13
- name: 'default',
14
- description: 'Versatile assistant (general + coding)',
15
- systemPrompt: '', // Uses the default skill
16
- },
17
-
18
- coder: {
19
- name: 'coder',
20
- description: 'Focused software engineer',
21
- systemPrompt: `You are a senior software engineer. Focus exclusively on code quality, architecture, and implementation.
22
-
23
- Behavior:
24
- - Write clean, production-grade code. No shortcuts.
25
- - Always read files before editing. Use edit_file for modifications.
26
- - Run tests after changes. Check types. Verify builds.
27
- - Match the project's existing patterns and conventions.
28
- - Be terse. Code speaks louder than explanations.`,
29
- },
30
-
31
- writer: {
32
- name: 'writer',
33
- description: 'Technical and creative writer',
34
- systemPrompt: `You are a skilled writer who adapts tone and style to the task.
35
-
36
- Behavior:
37
- - For technical writing: clear, structured, precise. Use headings, lists, examples.
38
- - For creative writing: vivid, engaging, varied sentence structure.
39
- - For emails/messages: concise, professional, direct.
40
- - Always match the language the user writes in.
41
- - Use fetch_url to research topics when needed.
42
- - Never pad with filler. Every sentence should carry weight.`,
43
- },
44
-
45
- researcher: {
46
- name: 'researcher',
47
- description: 'Deep research and analysis',
48
- systemPrompt: `You are a research analyst. Thorough, evidence-based, skeptical.
49
-
50
- Behavior:
51
- - Use fetch_url extensively to gather information from multiple sources.
52
- - Cross-reference claims. Note when sources conflict.
53
- - Structure findings with clear sections: Summary, Key Findings, Sources, Caveats.
54
- - Distinguish between facts, opinions, and speculation.
55
- - Always cite where you found information.
56
- - If you can't verify something, say so explicitly.`,
57
- },
58
-
59
- reviewer: {
60
- name: 'reviewer',
61
- description: 'Code review specialist',
62
- systemPrompt: `You are a meticulous code reviewer focused on quality, security, and maintainability.
63
-
64
- Behavior:
65
- - Read the entire file/diff before commenting.
66
- - Categorize issues: CRITICAL (bugs, security), WARNING (code smell, performance), SUGGESTION (style, readability).
67
- - Be specific. Show the line, explain the problem, suggest the fix.
68
- - Check for: error handling, input validation, edge cases, naming, complexity.
69
- - Don't nitpick formatting unless it affects readability.
70
- - Praise good patterns when you see them.`,
71
- },
72
-
73
- business: {
74
- name: 'business',
75
- description: 'Personal business assistant (Windows-focused)',
76
- systemPrompt: '', // Uses the business skill
77
- },
78
- }
79
-
80
- /**
81
- * Get a persona by name (case-insensitive).
82
- */
83
- export function getPersona(name: string): Persona | null {
84
- return PERSONAS[name.toLowerCase()] || null
85
- }
86
-
87
- /**
88
- * Format persona list for display.
89
- */
90
- export function formatPersonaList(current: string): string {
91
- const lines = ['Personas:']
92
- for (const [key, p] of Object.entries(PERSONAS)) {
93
- const marker = key === current ? ' *' : ' '
94
- lines.push(`${marker} ${key.padEnd(12)} ${p.description}`)
95
- }
96
- lines.push('')
97
- lines.push('Use: /persona <name>')
98
- return lines.join('\n')
99
- }
package/src/platform.ts DELETED
@@ -1,84 +0,0 @@
1
- import { existsSync } from 'node:fs'
2
-
3
- export const IS_WINDOWS = process.platform === 'win32'
4
- export const IS_MAC = process.platform === 'darwin'
5
- export const IS_LINUX = process.platform === 'linux'
6
-
7
- /**
8
- * Returns the shell command prefix for spawning subprocesses.
9
- * Windows: powershell, Unix: bash with sh fallback.
10
- */
11
- export function getShell(): [string, ...string[]] {
12
- if (IS_WINDOWS) {
13
- return ['powershell', '-NoProfile', '-NonInteractive', '-Command']
14
- }
15
-
16
- // Prefer user's SHELL, then bash, then sh
17
- const userShell = process.env.SHELL
18
- if (userShell && existsSync(userShell)) {
19
- return [userShell, '-c']
20
- }
21
-
22
- return ['bash', '-c']
23
- }
24
-
25
- /**
26
- * Returns a human-readable shell name for the system prompt.
27
- */
28
- export function getShellName(): string {
29
- if (IS_WINDOWS) return 'powershell'
30
- const shell = process.env.SHELL || '/bin/bash'
31
- const name = shell.split('/').pop() || 'bash'
32
- return name
33
- }
34
-
35
- /**
36
- * Check if a command is available on the system.
37
- */
38
- export async function commandExists(cmd: string): Promise<boolean> {
39
- try {
40
- const args = IS_WINDOWS
41
- ? ['powershell', '-NoProfile', '-Command', `Get-Command ${cmd} -ErrorAction SilentlyContinue`]
42
- : ['which', cmd]
43
-
44
- const proc = Bun.spawn(args, { stdout: 'pipe', stderr: 'pipe' })
45
- await proc.exited
46
- return proc.exitCode === 0
47
- } catch {
48
- return false
49
- }
50
- }
51
-
52
- // Cache ripgrep availability at startup
53
- let _hasRg: boolean | null = null
54
-
55
- export async function hasRipgrep(): Promise<boolean> {
56
- if (_hasRg !== null) return _hasRg
57
- _hasRg = await commandExists('rg')
58
- return _hasRg
59
- }
60
-
61
- /**
62
- * Directories to exclude from file searches.
63
- */
64
- export const SEARCH_EXCLUDES = [
65
- 'node_modules',
66
- '.git',
67
- 'dist',
68
- 'build',
69
- '.next',
70
- '__pycache__',
71
- '.venv',
72
- 'target',
73
- '.cache',
74
- ]
75
-
76
- /**
77
- * Check if a path should be excluded from search results.
78
- */
79
- export function shouldExclude(path: string): boolean {
80
- const normalized = path.replace(/\\/g, '/')
81
- return SEARCH_EXCLUDES.some(
82
- (ex) => normalized.includes(`/${ex}/`) || normalized.startsWith(`${ex}/`),
83
- )
84
- }