@stackmemoryai/stackmemory 0.5.33 → 0.5.34
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agents/core/agent-task-manager.js.map +1 -1
- package/dist/cli/commands/clear.js +1 -1
- package/dist/cli/commands/clear.js.map +1 -1
- package/dist/cli/commands/context.js +1 -1
- package/dist/cli/commands/context.js.map +1 -1
- package/dist/cli/commands/dashboard.js.map +1 -1
- package/dist/cli/commands/discovery.js +1 -1
- package/dist/cli/commands/discovery.js.map +1 -1
- package/dist/cli/commands/handoff.js +1 -1
- package/dist/cli/commands/handoff.js.map +1 -1
- package/dist/cli/commands/monitor.js +1 -1
- package/dist/cli/commands/monitor.js.map +1 -1
- package/dist/cli/commands/quality.js +1 -1
- package/dist/cli/commands/quality.js.map +1 -1
- package/dist/cli/commands/skills.js +1 -1
- package/dist/cli/commands/skills.js.map +1 -1
- package/dist/cli/commands/workflow.js +1 -1
- package/dist/cli/commands/workflow.js.map +1 -1
- package/dist/cli/commands/worktree.js +1 -1
- package/dist/cli/commands/worktree.js.map +1 -1
- package/dist/cli/index.js +1 -1
- package/dist/cli/index.js.map +1 -1
- package/dist/core/context/auto-context.js.map +1 -1
- package/dist/core/context/compaction-handler.js.map +2 -2
- package/dist/core/context/context-bridge.js.map +2 -2
- package/dist/core/context/dual-stack-manager.js +1 -1
- package/dist/core/context/dual-stack-manager.js.map +1 -1
- package/dist/core/context/enhanced-rehydration.js.map +1 -1
- package/dist/core/context/frame-database.js +8 -0
- package/dist/core/context/frame-database.js.map +2 -2
- package/dist/core/context/frame-handoff-manager.js.map +1 -1
- package/dist/core/context/frame-lifecycle-hooks.js +119 -0
- package/dist/core/context/frame-lifecycle-hooks.js.map +7 -0
- package/dist/core/context/frame-stack.js +29 -0
- package/dist/core/context/frame-stack.js.map +2 -2
- package/dist/core/context/incremental-gc.js.map +2 -2
- package/dist/core/context/index.js +4 -22
- package/dist/core/context/index.js.map +2 -2
- package/dist/core/context/refactored-frame-manager.js +140 -34
- package/dist/core/context/refactored-frame-manager.js.map +3 -3
- package/dist/core/context/shared-context-layer.js.map +1 -1
- package/dist/core/context/stack-merge-resolver.js.map +1 -1
- package/dist/core/database/database-adapter.js.map +1 -1
- package/dist/core/database/paradedb-adapter.js.map +1 -1
- package/dist/core/database/query-router.js.map +1 -1
- package/dist/core/database/sqlite-adapter.js.map +1 -1
- package/dist/core/digest/frame-digest-integration.js.map +1 -1
- package/dist/core/digest/hybrid-digest-generator.js.map +1 -1
- package/dist/core/digest/types.js.map +1 -1
- package/dist/core/errors/index.js +249 -0
- package/dist/core/errors/index.js.map +2 -2
- package/dist/core/frame/workflow-templates.js.map +2 -2
- package/dist/core/merge/conflict-detector.js.map +1 -1
- package/dist/core/merge/resolution-engine.js.map +1 -1
- package/dist/core/merge/stack-diff.js.map +1 -1
- package/dist/core/models/model-router.js +10 -1
- package/dist/core/models/model-router.js.map +2 -2
- package/dist/core/monitoring/error-handler.js +37 -270
- package/dist/core/monitoring/error-handler.js.map +3 -3
- package/dist/core/monitoring/session-monitor.js.map +1 -1
- package/dist/core/performance/lazy-context-loader.js.map +1 -1
- package/dist/core/performance/optimized-frame-context.js.map +1 -1
- package/dist/core/retrieval/context-retriever.js.map +1 -1
- package/dist/core/retrieval/graph-retrieval.js.map +1 -1
- package/dist/core/retrieval/hierarchical-retrieval.js.map +1 -1
- package/dist/core/retrieval/llm-context-retrieval.js.map +1 -1
- package/dist/core/retrieval/retrieval-benchmarks.js.map +1 -1
- package/dist/core/retrieval/summary-generator.js.map +1 -1
- package/dist/core/retrieval/types.js.map +1 -1
- package/dist/core/storage/chromadb-adapter.js.map +1 -1
- package/dist/core/storage/infinite-storage.js.map +1 -1
- package/dist/core/storage/two-tier-storage.js.map +1 -1
- package/dist/features/tasks/task-aware-context.js.map +1 -1
- package/dist/features/web/server/index.js +1 -1
- package/dist/features/web/server/index.js.map +1 -1
- package/dist/hooks/schemas.js +50 -0
- package/dist/hooks/schemas.js.map +2 -2
- package/dist/hooks/sms-action-runner.js +4 -0
- package/dist/hooks/sms-action-runner.js.map +2 -2
- package/dist/hooks/whatsapp-commands.js +142 -2
- package/dist/hooks/whatsapp-commands.js.map +2 -2
- package/dist/hooks/whatsapp-sync.js +34 -0
- package/dist/hooks/whatsapp-sync.js.map +2 -2
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/integrations/mcp/handlers/context-handlers.js.map +1 -1
- package/dist/integrations/mcp/handlers/discovery-handlers.js.map +1 -1
- package/dist/integrations/mcp/server.js +1 -1
- package/dist/integrations/mcp/server.js.map +1 -1
- package/dist/integrations/ralph/bridge/ralph-stackmemory-bridge.js +1 -1
- package/dist/integrations/ralph/bridge/ralph-stackmemory-bridge.js.map +1 -1
- package/dist/integrations/ralph/context/stackmemory-context-loader.js +1 -1
- package/dist/integrations/ralph/context/stackmemory-context-loader.js.map +1 -1
- package/dist/integrations/ralph/learning/pattern-learner.js +1 -1
- package/dist/integrations/ralph/learning/pattern-learner.js.map +1 -1
- package/dist/integrations/ralph/orchestration/multi-loop-orchestrator.js +1 -1
- package/dist/integrations/ralph/orchestration/multi-loop-orchestrator.js.map +1 -1
- package/dist/integrations/ralph/swarm/swarm-coordinator.js +1 -1
- package/dist/integrations/ralph/swarm/swarm-coordinator.js.map +1 -1
- package/dist/integrations/ralph/visualization/ralph-debugger.js +1 -1
- package/dist/integrations/ralph/visualization/ralph-debugger.js.map +1 -1
- package/dist/mcp/stackmemory-mcp-server.js +1 -1
- package/dist/mcp/stackmemory-mcp-server.js.map +1 -1
- package/dist/skills/claude-skills.js.map +1 -1
- package/dist/skills/recursive-agent-orchestrator.js.map +1 -1
- package/dist/skills/unified-rlm-orchestrator.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/hooks/schemas.ts"],
|
|
4
|
-
"sourcesContent": ["/**\n * Zod schemas for hook configuration validation\n * Prevents malformed or malicious configs from being loaded\n */\n\nimport { z } from 'zod';\nimport { logConfigInvalid } from './security-logger.js';\n\n// SMS/WhatsApp notification schemas\nexport const PromptOptionSchema = z.object({\n key: z.string().max(10),\n label: z.string().max(200),\n action: z.string().max(500).optional(),\n});\n\nexport const PendingPromptSchema = z.object({\n id: z.string().max(32),\n timestamp: z\n .string()\n .datetime({ offset: true })\n .or(z.string().regex(/^\\d{4}-\\d{2}-\\d{2}T/)),\n message: z.string().max(1000),\n options: z.array(PromptOptionSchema).max(10),\n type: z.enum(['options', 'yesno', 'freeform']),\n callback: z.string().max(500).optional(),\n expiresAt: z\n .string()\n .datetime({ offset: true })\n .or(z.string().regex(/^\\d{4}-\\d{2}-\\d{2}T/)),\n});\n\nexport const NotifyOnSchema = z.object({\n taskComplete: z.boolean(),\n reviewReady: z.boolean(),\n error: z.boolean(),\n custom: z.boolean(),\n contextSync: z.boolean().optional().default(true),\n});\n\nexport const QuietHoursSchema = z.object({\n enabled: z.boolean(),\n start: z.string().regex(/^\\d{2}:\\d{2}$/),\n end: z.string().regex(/^\\d{2}:\\d{2}$/),\n});\n\nexport const SMSConfigSchema = z.object({\n enabled: z.boolean(),\n channel: z.enum(['whatsapp', 'sms']),\n accountSid: z.string().max(100).optional(),\n authToken: z.string().max(100).optional(),\n smsFromNumber: z.string().max(20).optional(),\n smsToNumber: z.string().max(20).optional(),\n whatsappFromNumber: z.string().max(30).optional(),\n whatsappToNumber: z.string().max(30).optional(),\n fromNumber: z.string().max(20).optional(),\n toNumber: z.string().max(20).optional(),\n webhookUrl: z.string().url().max(500).optional(),\n notifyOn: NotifyOnSchema,\n quietHours: QuietHoursSchema.optional(),\n responseTimeout: z.number().int().min(30).max(3600),\n pendingPrompts: z.array(PendingPromptSchema).max(100),\n});\n\n// Action queue schemas\nexport const PendingActionSchema = z.object({\n id: z.string().max(32),\n promptId: z.string().max(32),\n response: z.string().max(1000),\n action: z.string().max(500),\n timestamp: z\n .string()\n .datetime({ offset: true })\n .or(z.string().regex(/^\\d{4}-\\d{2}-\\d{2}T/)),\n status: z.enum(['pending', 'running', 'completed', 'failed']),\n result: z.string().max(10000).optional(),\n error: z.string().max(1000).optional(),\n});\n\nexport const ActionQueueSchema = z.object({\n actions: z.array(PendingActionSchema).max(1000),\n lastChecked: z\n .string()\n .datetime({ offset: true })\n .or(z.string().regex(/^\\d{4}-\\d{2}-\\d{2}T/)),\n});\n\n// Auto-background config schema\nexport const AutoBackgroundConfigSchema = z.object({\n enabled: z.boolean(),\n timeoutMs: z.number().int().min(1000).max(600000),\n alwaysBackground: z.array(z.string().max(200)).max(100),\n neverBackground: z.array(z.string().max(200)).max(100),\n verbose: z.boolean().optional(),\n});\n\n// WhatsApp Sync Options schema\nexport const SyncOptionsSchema = z.object({\n autoSyncOnClose: z.boolean(),\n minFrameDuration: z.number().int().min(0).max(3600), // 0 to 1 hour\n includeDecisions: z.boolean(),\n includeFiles: z.boolean(),\n includeTests: z.boolean(),\n maxDigestLength: z.number().int().min(100).max(1000), // WhatsApp limit ~4096 chars\n});\n\n// WhatsApp Schedule Config schema\nexport const ScheduleConfigSchema = z.object({\n type: z.enum(['daily', 'hourly', 'interval']),\n time: z\n .string()\n .regex(/^\\d{2}:\\d{2}$/)\n .optional(), // \"HH:MM\" for daily\n intervalMinutes: z.number().int().min(5).max(1440).optional(), // 5 min to 24 hours\n includeInactive: z.boolean(), // Include when no activity\n quietHoursRespect: z.boolean(), // Respect quiet hours setting\n});\n\n// WhatsApp Schedule storage schema\nexport const ScheduleSchema = z.object({\n id: z.string().max(32),\n config: ScheduleConfigSchema,\n enabled: z.boolean(),\n lastRun: z\n .string()\n .datetime({ offset: true })\n .or(z.string().regex(/^\\d{4}-\\d{2}-\\d{2}T/))\n .optional(),\n nextRun: z\n .string()\n .datetime({ offset: true })\n .or(z.string().regex(/^\\d{4}-\\d{2}-\\d{2}T/))\n .optional(),\n createdAt: z\n .string()\n .datetime({ offset: true })\n .or(z.string().regex(/^\\d{4}-\\d{2}-\\d{2}T/)),\n});\n\nexport const ScheduleStorageSchema = z.object({\n schedules: z.array(ScheduleSchema).max(10),\n lastChecked: z\n .string()\n .datetime({ offset: true })\n .or(z.string().regex(/^\\d{4}-\\d{2}-\\d{2}T/)),\n});\n\n// WhatsApp Command schema\nexport const WhatsAppCommandSchema = z.object({\n name: z.string().max(50),\n description: z.string().max(200),\n enabled: z.boolean(),\n action: z.string().max(500).optional(), // Safe action to execute\n requiresArg: z.boolean().optional(),\n argPattern: z.string().max(100).optional(), // Regex pattern for arg validation\n});\n\nexport const WhatsAppCommandsConfigSchema = z.object({\n enabled: z.boolean(),\n commands: z.array(WhatsAppCommandSchema).max(50),\n});\n\n// Type exports\nexport type SMSConfigValidated = z.infer<typeof SMSConfigSchema>;\nexport type ActionQueueValidated = z.infer<typeof ActionQueueSchema>;\nexport type AutoBackgroundConfigValidated = z.infer<\n typeof AutoBackgroundConfigSchema\n>;\nexport type SyncOptionsValidated = z.infer<typeof SyncOptionsSchema>;\nexport type ScheduleConfigValidated = z.infer<typeof ScheduleConfigSchema>;\nexport type ScheduleValidated = z.infer<typeof ScheduleSchema>;\nexport type ScheduleStorageValidated = z.infer<typeof ScheduleStorageSchema>;\nexport type WhatsAppCommandValidated = z.infer<typeof WhatsAppCommandSchema>;\nexport type WhatsAppCommandsConfigValidated = z.infer<\n typeof WhatsAppCommandsConfigSchema\n>;\n\n/**\n * Safely parse and validate config, returning default on failure\n */\nexport function parseConfigSafe<T>(\n schema: z.ZodSchema<T>,\n data: unknown,\n defaultValue: T,\n configName: string\n): T {\n const result = schema.safeParse(data);\n if (result.success) {\n return result.data;\n }\n const errors = result.error.issues.map(\n (i) => `${i.path.join('.')}: ${i.message}`\n );\n logConfigInvalid(configName, errors);\n console.error(`[hooks] Invalid ${configName} config:`, errors.join(', '));\n return defaultValue;\n}\n"],
|
|
5
|
-
"mappings": "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
|
|
4
|
+
"sourcesContent": ["/**\n * Zod schemas for hook configuration validation\n * Prevents malformed or malicious configs from being loaded\n */\n\nimport { z } from 'zod';\nimport { logConfigInvalid } from './security-logger.js';\n\n// SMS/WhatsApp notification schemas\nexport const PromptOptionSchema = z.object({\n key: z.string().max(10),\n label: z.string().max(200),\n action: z.string().max(500).optional(),\n});\n\nexport const PendingPromptSchema = z.object({\n id: z.string().max(32),\n timestamp: z\n .string()\n .datetime({ offset: true })\n .or(z.string().regex(/^\\d{4}-\\d{2}-\\d{2}T/)),\n message: z.string().max(1000),\n options: z.array(PromptOptionSchema).max(10),\n type: z.enum(['options', 'yesno', 'freeform']),\n callback: z.string().max(500).optional(),\n expiresAt: z\n .string()\n .datetime({ offset: true })\n .or(z.string().regex(/^\\d{4}-\\d{2}-\\d{2}T/)),\n});\n\nexport const NotifyOnSchema = z.object({\n taskComplete: z.boolean(),\n reviewReady: z.boolean(),\n error: z.boolean(),\n custom: z.boolean(),\n contextSync: z.boolean().optional().default(true),\n});\n\nexport const QuietHoursSchema = z.object({\n enabled: z.boolean(),\n start: z.string().regex(/^\\d{2}:\\d{2}$/),\n end: z.string().regex(/^\\d{2}:\\d{2}$/),\n});\n\nexport const SMSConfigSchema = z.object({\n enabled: z.boolean(),\n channel: z.enum(['whatsapp', 'sms']),\n accountSid: z.string().max(100).optional(),\n authToken: z.string().max(100).optional(),\n smsFromNumber: z.string().max(20).optional(),\n smsToNumber: z.string().max(20).optional(),\n whatsappFromNumber: z.string().max(30).optional(),\n whatsappToNumber: z.string().max(30).optional(),\n fromNumber: z.string().max(20).optional(),\n toNumber: z.string().max(20).optional(),\n webhookUrl: z.string().url().max(500).optional(),\n notifyOn: NotifyOnSchema,\n quietHours: QuietHoursSchema.optional(),\n responseTimeout: z.number().int().min(30).max(3600),\n pendingPrompts: z.array(PendingPromptSchema).max(100),\n});\n\n// Action queue schemas\nexport const PendingActionSchema = z.object({\n id: z.string().max(32),\n promptId: z.string().max(32),\n response: z.string().max(1000),\n action: z.string().max(500),\n timestamp: z\n .string()\n .datetime({ offset: true })\n .or(z.string().regex(/^\\d{4}-\\d{2}-\\d{2}T/)),\n status: z.enum(['pending', 'running', 'completed', 'failed']),\n result: z.string().max(10000).optional(),\n error: z.string().max(1000).optional(),\n});\n\nexport const ActionQueueSchema = z.object({\n actions: z.array(PendingActionSchema).max(1000),\n lastChecked: z\n .string()\n .datetime({ offset: true })\n .or(z.string().regex(/^\\d{4}-\\d{2}-\\d{2}T/)),\n});\n\n// Auto-background config schema\nexport const AutoBackgroundConfigSchema = z.object({\n enabled: z.boolean(),\n timeoutMs: z.number().int().min(1000).max(600000),\n alwaysBackground: z.array(z.string().max(200)).max(100),\n neverBackground: z.array(z.string().max(200)).max(100),\n verbose: z.boolean().optional(),\n});\n\n// WhatsApp Sync Options schema\nexport const SyncOptionsSchema = z.object({\n autoSyncOnClose: z.boolean(),\n minFrameDuration: z.number().int().min(0).max(3600), // 0 to 1 hour\n includeDecisions: z.boolean(),\n includeFiles: z.boolean(),\n includeTests: z.boolean(),\n maxDigestLength: z.number().int().min(100).max(1000), // WhatsApp limit ~4096 chars\n});\n\n// WhatsApp Schedule Config schema\nexport const ScheduleConfigSchema = z.object({\n type: z.enum(['daily', 'hourly', 'interval']),\n time: z\n .string()\n .regex(/^\\d{2}:\\d{2}$/)\n .optional(), // \"HH:MM\" for daily\n intervalMinutes: z.number().int().min(5).max(1440).optional(), // 5 min to 24 hours\n includeInactive: z.boolean(), // Include when no activity\n quietHoursRespect: z.boolean(), // Respect quiet hours setting\n});\n\n// WhatsApp Schedule storage schema\nexport const ScheduleSchema = z.object({\n id: z.string().max(32),\n config: ScheduleConfigSchema,\n enabled: z.boolean(),\n lastRun: z\n .string()\n .datetime({ offset: true })\n .or(z.string().regex(/^\\d{4}-\\d{2}-\\d{2}T/))\n .optional(),\n nextRun: z\n .string()\n .datetime({ offset: true })\n .or(z.string().regex(/^\\d{4}-\\d{2}-\\d{2}T/))\n .optional(),\n createdAt: z\n .string()\n .datetime({ offset: true })\n .or(z.string().regex(/^\\d{4}-\\d{2}-\\d{2}T/)),\n});\n\nexport const ScheduleStorageSchema = z.object({\n schedules: z.array(ScheduleSchema).max(10),\n lastChecked: z\n .string()\n .datetime({ offset: true })\n .or(z.string().regex(/^\\d{4}-\\d{2}-\\d{2}T/)),\n});\n\n// WhatsApp Command schema\nexport const WhatsAppCommandSchema = z.object({\n name: z.string().max(50),\n description: z.string().max(200),\n enabled: z.boolean(),\n action: z.string().max(500).optional(), // Safe action to execute\n requiresArg: z.boolean().optional(),\n argPattern: z.string().max(100).optional(), // Regex pattern for arg validation\n});\n\nexport const WhatsAppCommandsConfigSchema = z.object({\n enabled: z.boolean(),\n commands: z.array(WhatsAppCommandSchema).max(50),\n});\n\n// Model Router schemas\nexport const ModelProviderSchema = z.enum([\n 'anthropic',\n 'qwen',\n 'openai',\n 'ollama',\n 'custom',\n]);\n\nexport const ModelConfigSchema = z.object({\n provider: ModelProviderSchema,\n model: z.string().max(100),\n baseUrl: z.string().url().max(500).optional(),\n apiKeyEnv: z.string().max(100),\n headers: z.record(z.string().max(500)).optional(),\n params: z.record(z.unknown()).optional(),\n});\n\nexport const ModelRouterConfigSchema = z.object({\n enabled: z.boolean(),\n defaultProvider: ModelProviderSchema,\n taskRouting: z\n .object({\n plan: ModelProviderSchema.optional(),\n think: ModelProviderSchema.optional(),\n code: ModelProviderSchema.optional(),\n review: ModelProviderSchema.optional(),\n })\n .optional()\n .default({}),\n fallback: z.object({\n enabled: z.boolean(),\n provider: ModelProviderSchema,\n onRateLimit: z.boolean(),\n onError: z.boolean(),\n onTimeout: z.boolean(),\n maxRetries: z.number().int().min(0).max(10),\n retryDelayMs: z.number().int().min(100).max(30000),\n }),\n providers: z\n .object({\n anthropic: ModelConfigSchema.optional(),\n qwen: ModelConfigSchema.optional(),\n openai: ModelConfigSchema.optional(),\n ollama: ModelConfigSchema.optional(),\n custom: ModelConfigSchema.optional(),\n })\n .optional()\n .default({}),\n thinkingMode: z.object({\n enabled: z.boolean(),\n budget: z.number().int().min(1000).max(100000).optional(),\n temperature: z.number().min(0).max(2).optional(),\n topP: z.number().min(0).max(1).optional(),\n }),\n});\n\n// Type exports\nexport type ModelRouterConfigValidated = z.infer<\n typeof ModelRouterConfigSchema\n>;\nexport type SMSConfigValidated = z.infer<typeof SMSConfigSchema>;\nexport type ActionQueueValidated = z.infer<typeof ActionQueueSchema>;\nexport type AutoBackgroundConfigValidated = z.infer<\n typeof AutoBackgroundConfigSchema\n>;\nexport type SyncOptionsValidated = z.infer<typeof SyncOptionsSchema>;\nexport type ScheduleConfigValidated = z.infer<typeof ScheduleConfigSchema>;\nexport type ScheduleValidated = z.infer<typeof ScheduleSchema>;\nexport type ScheduleStorageValidated = z.infer<typeof ScheduleStorageSchema>;\nexport type WhatsAppCommandValidated = z.infer<typeof WhatsAppCommandSchema>;\nexport type WhatsAppCommandsConfigValidated = z.infer<\n typeof WhatsAppCommandsConfigSchema\n>;\n\n/**\n * Safely parse and validate config, returning default on failure\n */\nexport function parseConfigSafe<T>(\n schema: z.ZodSchema<T>,\n data: unknown,\n defaultValue: T,\n configName: string\n): T {\n const result = schema.safeParse(data);\n if (result.success) {\n return result.data;\n }\n const errors = result.error.issues.map(\n (i) => `${i.path.join('.')}: ${i.message}`\n );\n logConfigInvalid(configName, errors);\n console.error(`[hooks] Invalid ${configName} config:`, errors.join(', '));\n return defaultValue;\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -41,6 +41,10 @@ const SAFE_ACTION_PATTERNS = [
|
|
|
41
41
|
{ pattern: /^context$/i },
|
|
42
42
|
{ pattern: /^help$/i },
|
|
43
43
|
{ pattern: /^sync$/i },
|
|
44
|
+
// Claude Code launcher
|
|
45
|
+
{ pattern: /^claude-sm$/ },
|
|
46
|
+
// Log viewing (safe read-only)
|
|
47
|
+
{ pattern: /^tail -\d+ ~\/\.claude\/logs\/\*\.log$/ },
|
|
44
48
|
// Custom aliases (cwm = claude worktree merge)
|
|
45
49
|
{ pattern: /^cwm$/ },
|
|
46
50
|
// Simple echo/confirmation (no variables)
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/hooks/sms-action-runner.ts"],
|
|
4
|
-
"sourcesContent": ["/**\n * SMS Action Runner - Executes actions based on SMS responses\n * Bridges SMS responses to Claude Code actions\n *\n * Security: Uses allowlist-based action execution to prevent command injection\n */\n\nimport { existsSync, readFileSync } from 'fs';\nimport { join } from 'path';\nimport { homedir } from 'os';\nimport { execFileSync } from 'child_process';\nimport { randomBytes } from 'crypto';\nimport { writeFileSecure, ensureSecureDir } from './secure-fs.js';\nimport { ActionQueueSchema, parseConfigSafe } from './schemas.js';\nimport { LinearClient } from '../integrations/linear/client.js';\nimport { LinearAuthManager } from '../integrations/linear/auth.js';\n\n// Allowlist of safe action patterns\nconst SAFE_ACTION_PATTERNS: Array<{\n pattern: RegExp;\n validate?: (match: RegExpMatchArray) => boolean;\n}> = [\n // Git/GitHub CLI commands (limited to safe operations)\n { pattern: /^gh pr (view|list|status|checks) (\\d+)$/ },\n { pattern: /^gh pr review (\\d+) --approve$/ },\n { pattern: /^gh pr merge (\\d+) --squash$/ },\n { pattern: /^gh issue (view|list) (\\d+)?$/ },\n\n // NPM commands (limited to safe operations)\n { pattern: /^npm run (build|test|lint|lint:fix|test:run)$/ },\n { pattern: /^npm (test|run build)$/ },\n\n // StackMemory commands\n { pattern: /^stackmemory (status|notify check|context list)$/ },\n // Task start with optional --assign-me flag (Linear task ID is UUID format)\n {\n pattern: /^stackmemory task start ([a-f0-9-]{36})( --assign-me)?$/,\n },\n // Additional StackMemory commands for mobile/WhatsApp\n { pattern: /^stackmemory context show$/ },\n { pattern: /^stackmemory task list$/ },\n\n // Git commands\n { pattern: /^git (status|diff|log|branch)( --[a-z-]+)*$/ },\n { pattern: /^git add -A && git commit$/ },\n { pattern: /^gh pr create --fill$/ },\n // Git log with line limit for mobile-friendly output\n { pattern: /^git log --oneline -\\d{1,2}$/ },\n\n // WhatsApp/Mobile quick commands\n { pattern: /^status$/i },\n { pattern: /^tasks$/i },\n { pattern: /^context$/i },\n { pattern: /^help$/i },\n { pattern: /^sync$/i },\n\n // Custom aliases (cwm = claude worktree merge)\n { pattern: /^cwm$/ },\n\n // Simple echo/confirmation (no variables)\n {\n pattern:\n /^echo \"?(Done|OK|Confirmed|Acknowledged|Great work! Time for a coffee break\\.)\"?$/,\n },\n];\n\n/**\n * Check if an action is in the allowlist\n */\nfunction isActionAllowed(action: string): boolean {\n const trimmed = action.trim();\n return SAFE_ACTION_PATTERNS.some(({ pattern, validate }) => {\n const match = trimmed.match(pattern);\n if (!match) return false;\n if (validate && !validate(match)) return false;\n return true;\n });\n}\n\nexport interface PendingAction {\n id: string;\n promptId: string;\n response: string;\n action: string;\n timestamp: string;\n status: 'pending' | 'running' | 'completed' | 'failed';\n result?: string;\n error?: string;\n}\n\nexport interface ActionQueue {\n actions: PendingAction[];\n lastChecked: string;\n}\n\nconst QUEUE_PATH = join(homedir(), '.stackmemory', 'sms-action-queue.json');\n\nconst DEFAULT_QUEUE: ActionQueue = {\n actions: [],\n lastChecked: new Date().toISOString(),\n};\n\nexport function loadActionQueue(): ActionQueue {\n try {\n if (existsSync(QUEUE_PATH)) {\n const data = JSON.parse(readFileSync(QUEUE_PATH, 'utf8'));\n return parseConfigSafe(\n ActionQueueSchema,\n data,\n DEFAULT_QUEUE,\n 'action-queue'\n );\n }\n } catch {\n // Use defaults\n }\n return { ...DEFAULT_QUEUE, lastChecked: new Date().toISOString() };\n}\n\nexport function saveActionQueue(queue: ActionQueue): void {\n try {\n ensureSecureDir(join(homedir(), '.stackmemory'));\n writeFileSecure(QUEUE_PATH, JSON.stringify(queue, null, 2));\n } catch {\n // Silently fail\n }\n}\n\nexport function queueAction(\n promptId: string,\n response: string,\n action: string\n): string {\n const queue = loadActionQueue();\n // Use cryptographically secure random ID\n const id = randomBytes(8).toString('hex');\n\n queue.actions.push({\n id,\n promptId,\n response,\n action,\n timestamp: new Date().toISOString(),\n status: 'pending',\n });\n\n saveActionQueue(queue);\n return id;\n}\n\n/**\n * Get Linear client if available\n * Returns null if credentials are missing or invalid\n */\nfunction getLinearClient(): LinearClient | null {\n // Try API key first - must be valid format (lin_api_*)\n const apiKey = process.env['LINEAR_API_KEY'];\n if (apiKey && apiKey.startsWith('lin_api_')) {\n return new LinearClient({ apiKey });\n }\n\n try {\n const authManager = new LinearAuthManager();\n const tokens = authManager.loadTokens();\n if (tokens?.accessToken) {\n return new LinearClient({ accessToken: tokens.accessToken });\n }\n } catch {\n // Auth not available\n }\n\n return null;\n}\n\n/**\n * Handle special actions that require API calls instead of shell commands\n */\nasync function handleSpecialAction(action: string): Promise<{\n handled: boolean;\n success?: boolean;\n output?: string;\n error?: string;\n}> {\n // Handle stackmemory task start command\n const taskStartMatch = action.match(\n /^stackmemory task start ([a-f0-9-]{36})( --assign-me)?$/\n );\n if (taskStartMatch) {\n const issueId = taskStartMatch[1];\n const client = getLinearClient();\n\n if (!client) {\n return {\n handled: true,\n success: false,\n error:\n 'Linear not configured. Set LINEAR_API_KEY or run stackmemory linear setup.',\n };\n }\n\n try {\n const result = await client.startIssue(issueId);\n if (result.success && result.issue) {\n return {\n handled: true,\n success: true,\n output: `Started: ${result.issue.identifier} - ${result.issue.title}`,\n };\n }\n return {\n handled: true,\n success: false,\n error: result.error || 'Failed to start issue',\n };\n } catch (err) {\n return {\n handled: true,\n success: false,\n error: err instanceof Error ? err.message : 'Unknown error',\n };\n }\n }\n\n return { handled: false };\n}\n\n/**\n * Execute an action safely using allowlist validation\n * This prevents command injection by only allowing pre-approved commands\n */\nexport async function executeActionSafe(\n action: string,\n _response: string\n): Promise<{ success: boolean; output?: string; error?: string }> {\n // Check if action is in allowlist\n if (!isActionAllowed(action)) {\n console.error(`[sms-action] Action not in allowlist: ${action}`);\n return {\n success: false,\n error: `Action not allowed. Only pre-approved commands can be executed via SMS.`,\n };\n }\n\n // Check for special actions that need API calls\n const specialResult = await handleSpecialAction(action);\n if (specialResult.handled) {\n return {\n success: specialResult.success || false,\n output: specialResult.output,\n error: specialResult.error,\n };\n }\n\n try {\n console.log(`[sms-action] Executing safe action: ${action}`);\n\n // Parse the action into command and args\n const parts = action.split(' ');\n const cmd = parts[0];\n const args = parts.slice(1);\n\n // Use execFileSync for commands without shell interpretation\n // This prevents shell injection even if the allowlist is somehow bypassed\n const output = execFileSync(cmd, args, {\n encoding: 'utf8',\n timeout: 60000,\n stdio: ['pipe', 'pipe', 'pipe'],\n shell: false, // Explicitly disable shell\n });\n\n return { success: true, output };\n } catch (err) {\n const error = err instanceof Error ? err.message : String(err);\n return { success: false, error };\n }\n}\n\nexport function getPendingActions(): PendingAction[] {\n const queue = loadActionQueue();\n return queue.actions.filter((a) => a.status === 'pending');\n}\n\nexport function markActionRunning(id: string): void {\n const queue = loadActionQueue();\n const action = queue.actions.find((a) => a.id === id);\n if (action) {\n action.status = 'running';\n saveActionQueue(queue);\n }\n}\n\nexport function markActionCompleted(\n id: string,\n result?: string,\n error?: string\n): void {\n const queue = loadActionQueue();\n const action = queue.actions.find((a) => a.id === id);\n if (action) {\n action.status = error ? 'failed' : 'completed';\n action.result = result;\n action.error = error;\n saveActionQueue(queue);\n }\n}\n\nexport async function executeAction(action: PendingAction): Promise<{\n success: boolean;\n output?: string;\n error?: string;\n}> {\n markActionRunning(action.id);\n\n // Use the safe execution path to prevent command injection\n const result = await executeActionSafe(action.action, action.response);\n\n if (result.success) {\n markActionCompleted(action.id, result.output);\n } else {\n markActionCompleted(action.id, undefined, result.error);\n }\n\n return result;\n}\n\nexport async function processAllPendingActions(): Promise<{\n processed: number;\n succeeded: number;\n failed: number;\n}> {\n const pending = getPendingActions();\n let succeeded = 0;\n let failed = 0;\n\n for (const action of pending) {\n const result = await executeAction(action);\n if (result.success) {\n succeeded++;\n } else {\n failed++;\n }\n }\n\n return { processed: pending.length, succeeded, failed };\n}\n\n// Clean up old completed actions (keep last 50)\nexport function cleanupOldActions(): number {\n const queue = loadActionQueue();\n const completed = queue.actions.filter(\n (a) => a.status === 'completed' || a.status === 'failed'\n );\n\n if (completed.length > 50) {\n const toRemove = completed.slice(0, completed.length - 50);\n queue.actions = queue.actions.filter(\n (a) => !toRemove.find((r) => r.id === a.id)\n );\n saveActionQueue(queue);\n return toRemove.length;\n }\n\n return 0;\n}\n\n/**\n * Action Templates - Common actions for SMS responses\n *\n * SECURITY NOTE: These templates return command strings that must be\n * validated against SAFE_ACTION_PATTERNS before execution.\n * Templates that accept user input are removed to prevent injection.\n */\nexport const ACTION_TEMPLATES = {\n // Git/PR actions (PR numbers must be validated as integers)\n approvePR: (prNumber: string) => {\n // Validate PR number is numeric only\n if (!/^\\d+$/.test(prNumber)) {\n throw new Error('Invalid PR number');\n }\n return `gh pr review ${prNumber} --approve`;\n },\n mergePR: (prNumber: string) => {\n if (!/^\\d+$/.test(prNumber)) {\n throw new Error('Invalid PR number');\n }\n return `gh pr merge ${prNumber} --squash`;\n },\n viewPR: (prNumber: string) => {\n if (!/^\\d+$/.test(prNumber)) {\n throw new Error('Invalid PR number');\n }\n return `gh pr view ${prNumber}`;\n },\n\n // Build actions (no user input)\n rebuild: () => `npm run build`,\n retest: () => `npm run test:run`,\n lint: () => `npm run lint:fix`,\n\n // Status actions (no user input)\n status: () => `stackmemory status`,\n checkNotifications: () => `stackmemory notify check`,\n\n // REMOVED for security - these templates allowed arbitrary user input:\n // - requestChanges (allowed arbitrary message)\n // - closePR (could be used maliciously)\n // - deploy/rollback (too dangerous for SMS)\n // - verifyDeployment (allowed arbitrary URL)\n // - notifySlack (allowed arbitrary message - command injection)\n // - notifyTeam (allowed arbitrary message - command injection)\n};\n\n/**\n * Create action string from template\n */\nexport function createAction(\n template: keyof typeof ACTION_TEMPLATES,\n ...args: string[]\n): string {\n const fn = ACTION_TEMPLATES[template];\n if (typeof fn === 'function') {\n return (fn as (...args: string[]) => string)(...args);\n }\n return fn;\n}\n\n/**\n * Watch for new actions and execute them\n */\nexport function startActionWatcher(intervalMs: number = 5000): NodeJS.Timeout {\n console.log(\n `[sms-action] Starting action watcher (interval: ${intervalMs}ms)`\n );\n\n return setInterval(() => {\n const pending = getPendingActions();\n if (pending.length > 0) {\n console.log(`[sms-action] Found ${pending.length} pending action(s)`);\n processAllPendingActions();\n }\n }, intervalMs);\n}\n\n/**\n * Integration with SMS webhook - queue action when response received\n */\nexport function handleSMSResponse(\n promptId: string,\n response: string,\n action?: string\n): void {\n if (action) {\n const actionId = queueAction(promptId, response, action);\n console.log(`[sms-action] Queued action ${actionId}: ${action}`);\n }\n}\n"],
|
|
5
|
-
"mappings": "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",
|
|
4
|
+
"sourcesContent": ["/**\n * SMS Action Runner - Executes actions based on SMS responses\n * Bridges SMS responses to Claude Code actions\n *\n * Security: Uses allowlist-based action execution to prevent command injection\n */\n\nimport { existsSync, readFileSync } from 'fs';\nimport { join } from 'path';\nimport { homedir } from 'os';\nimport { execFileSync } from 'child_process';\nimport { randomBytes } from 'crypto';\nimport { writeFileSecure, ensureSecureDir } from './secure-fs.js';\nimport { ActionQueueSchema, parseConfigSafe } from './schemas.js';\nimport { LinearClient } from '../integrations/linear/client.js';\nimport { LinearAuthManager } from '../integrations/linear/auth.js';\n\n// Allowlist of safe action patterns\nconst SAFE_ACTION_PATTERNS: Array<{\n pattern: RegExp;\n validate?: (match: RegExpMatchArray) => boolean;\n}> = [\n // Git/GitHub CLI commands (limited to safe operations)\n { pattern: /^gh pr (view|list|status|checks) (\\d+)$/ },\n { pattern: /^gh pr review (\\d+) --approve$/ },\n { pattern: /^gh pr merge (\\d+) --squash$/ },\n { pattern: /^gh issue (view|list) (\\d+)?$/ },\n\n // NPM commands (limited to safe operations)\n { pattern: /^npm run (build|test|lint|lint:fix|test:run)$/ },\n { pattern: /^npm (test|run build)$/ },\n\n // StackMemory commands\n { pattern: /^stackmemory (status|notify check|context list)$/ },\n // Task start with optional --assign-me flag (Linear task ID is UUID format)\n {\n pattern: /^stackmemory task start ([a-f0-9-]{36})( --assign-me)?$/,\n },\n // Additional StackMemory commands for mobile/WhatsApp\n { pattern: /^stackmemory context show$/ },\n { pattern: /^stackmemory task list$/ },\n\n // Git commands\n { pattern: /^git (status|diff|log|branch)( --[a-z-]+)*$/ },\n { pattern: /^git add -A && git commit$/ },\n { pattern: /^gh pr create --fill$/ },\n // Git log with line limit for mobile-friendly output\n { pattern: /^git log --oneline -\\d{1,2}$/ },\n\n // WhatsApp/Mobile quick commands\n { pattern: /^status$/i },\n { pattern: /^tasks$/i },\n { pattern: /^context$/i },\n { pattern: /^help$/i },\n { pattern: /^sync$/i },\n\n // Claude Code launcher\n { pattern: /^claude-sm$/ },\n\n // Log viewing (safe read-only)\n { pattern: /^tail -\\d+ ~\\/\\.claude\\/logs\\/\\*\\.log$/ },\n\n // Custom aliases (cwm = claude worktree merge)\n { pattern: /^cwm$/ },\n\n // Simple echo/confirmation (no variables)\n {\n pattern:\n /^echo \"?(Done|OK|Confirmed|Acknowledged|Great work! Time for a coffee break\\.)\"?$/,\n },\n];\n\n/**\n * Check if an action is in the allowlist\n */\nfunction isActionAllowed(action: string): boolean {\n const trimmed = action.trim();\n return SAFE_ACTION_PATTERNS.some(({ pattern, validate }) => {\n const match = trimmed.match(pattern);\n if (!match) return false;\n if (validate && !validate(match)) return false;\n return true;\n });\n}\n\nexport interface PendingAction {\n id: string;\n promptId: string;\n response: string;\n action: string;\n timestamp: string;\n status: 'pending' | 'running' | 'completed' | 'failed';\n result?: string;\n error?: string;\n}\n\nexport interface ActionQueue {\n actions: PendingAction[];\n lastChecked: string;\n}\n\nconst QUEUE_PATH = join(homedir(), '.stackmemory', 'sms-action-queue.json');\n\nconst DEFAULT_QUEUE: ActionQueue = {\n actions: [],\n lastChecked: new Date().toISOString(),\n};\n\nexport function loadActionQueue(): ActionQueue {\n try {\n if (existsSync(QUEUE_PATH)) {\n const data = JSON.parse(readFileSync(QUEUE_PATH, 'utf8'));\n return parseConfigSafe(\n ActionQueueSchema,\n data,\n DEFAULT_QUEUE,\n 'action-queue'\n );\n }\n } catch {\n // Use defaults\n }\n return { ...DEFAULT_QUEUE, lastChecked: new Date().toISOString() };\n}\n\nexport function saveActionQueue(queue: ActionQueue): void {\n try {\n ensureSecureDir(join(homedir(), '.stackmemory'));\n writeFileSecure(QUEUE_PATH, JSON.stringify(queue, null, 2));\n } catch {\n // Silently fail\n }\n}\n\nexport function queueAction(\n promptId: string,\n response: string,\n action: string\n): string {\n const queue = loadActionQueue();\n // Use cryptographically secure random ID\n const id = randomBytes(8).toString('hex');\n\n queue.actions.push({\n id,\n promptId,\n response,\n action,\n timestamp: new Date().toISOString(),\n status: 'pending',\n });\n\n saveActionQueue(queue);\n return id;\n}\n\n/**\n * Get Linear client if available\n * Returns null if credentials are missing or invalid\n */\nfunction getLinearClient(): LinearClient | null {\n // Try API key first - must be valid format (lin_api_*)\n const apiKey = process.env['LINEAR_API_KEY'];\n if (apiKey && apiKey.startsWith('lin_api_')) {\n return new LinearClient({ apiKey });\n }\n\n try {\n const authManager = new LinearAuthManager();\n const tokens = authManager.loadTokens();\n if (tokens?.accessToken) {\n return new LinearClient({ accessToken: tokens.accessToken });\n }\n } catch {\n // Auth not available\n }\n\n return null;\n}\n\n/**\n * Handle special actions that require API calls instead of shell commands\n */\nasync function handleSpecialAction(action: string): Promise<{\n handled: boolean;\n success?: boolean;\n output?: string;\n error?: string;\n}> {\n // Handle stackmemory task start command\n const taskStartMatch = action.match(\n /^stackmemory task start ([a-f0-9-]{36})( --assign-me)?$/\n );\n if (taskStartMatch) {\n const issueId = taskStartMatch[1];\n const client = getLinearClient();\n\n if (!client) {\n return {\n handled: true,\n success: false,\n error:\n 'Linear not configured. Set LINEAR_API_KEY or run stackmemory linear setup.',\n };\n }\n\n try {\n const result = await client.startIssue(issueId);\n if (result.success && result.issue) {\n return {\n handled: true,\n success: true,\n output: `Started: ${result.issue.identifier} - ${result.issue.title}`,\n };\n }\n return {\n handled: true,\n success: false,\n error: result.error || 'Failed to start issue',\n };\n } catch (err) {\n return {\n handled: true,\n success: false,\n error: err instanceof Error ? err.message : 'Unknown error',\n };\n }\n }\n\n return { handled: false };\n}\n\n/**\n * Execute an action safely using allowlist validation\n * This prevents command injection by only allowing pre-approved commands\n */\nexport async function executeActionSafe(\n action: string,\n _response: string\n): Promise<{ success: boolean; output?: string; error?: string }> {\n // Check if action is in allowlist\n if (!isActionAllowed(action)) {\n console.error(`[sms-action] Action not in allowlist: ${action}`);\n return {\n success: false,\n error: `Action not allowed. Only pre-approved commands can be executed via SMS.`,\n };\n }\n\n // Check for special actions that need API calls\n const specialResult = await handleSpecialAction(action);\n if (specialResult.handled) {\n return {\n success: specialResult.success || false,\n output: specialResult.output,\n error: specialResult.error,\n };\n }\n\n try {\n console.log(`[sms-action] Executing safe action: ${action}`);\n\n // Parse the action into command and args\n const parts = action.split(' ');\n const cmd = parts[0];\n const args = parts.slice(1);\n\n // Use execFileSync for commands without shell interpretation\n // This prevents shell injection even if the allowlist is somehow bypassed\n const output = execFileSync(cmd, args, {\n encoding: 'utf8',\n timeout: 60000,\n stdio: ['pipe', 'pipe', 'pipe'],\n shell: false, // Explicitly disable shell\n });\n\n return { success: true, output };\n } catch (err) {\n const error = err instanceof Error ? err.message : String(err);\n return { success: false, error };\n }\n}\n\nexport function getPendingActions(): PendingAction[] {\n const queue = loadActionQueue();\n return queue.actions.filter((a) => a.status === 'pending');\n}\n\nexport function markActionRunning(id: string): void {\n const queue = loadActionQueue();\n const action = queue.actions.find((a) => a.id === id);\n if (action) {\n action.status = 'running';\n saveActionQueue(queue);\n }\n}\n\nexport function markActionCompleted(\n id: string,\n result?: string,\n error?: string\n): void {\n const queue = loadActionQueue();\n const action = queue.actions.find((a) => a.id === id);\n if (action) {\n action.status = error ? 'failed' : 'completed';\n action.result = result;\n action.error = error;\n saveActionQueue(queue);\n }\n}\n\nexport async function executeAction(action: PendingAction): Promise<{\n success: boolean;\n output?: string;\n error?: string;\n}> {\n markActionRunning(action.id);\n\n // Use the safe execution path to prevent command injection\n const result = await executeActionSafe(action.action, action.response);\n\n if (result.success) {\n markActionCompleted(action.id, result.output);\n } else {\n markActionCompleted(action.id, undefined, result.error);\n }\n\n return result;\n}\n\nexport async function processAllPendingActions(): Promise<{\n processed: number;\n succeeded: number;\n failed: number;\n}> {\n const pending = getPendingActions();\n let succeeded = 0;\n let failed = 0;\n\n for (const action of pending) {\n const result = await executeAction(action);\n if (result.success) {\n succeeded++;\n } else {\n failed++;\n }\n }\n\n return { processed: pending.length, succeeded, failed };\n}\n\n// Clean up old completed actions (keep last 50)\nexport function cleanupOldActions(): number {\n const queue = loadActionQueue();\n const completed = queue.actions.filter(\n (a) => a.status === 'completed' || a.status === 'failed'\n );\n\n if (completed.length > 50) {\n const toRemove = completed.slice(0, completed.length - 50);\n queue.actions = queue.actions.filter(\n (a) => !toRemove.find((r) => r.id === a.id)\n );\n saveActionQueue(queue);\n return toRemove.length;\n }\n\n return 0;\n}\n\n/**\n * Action Templates - Common actions for SMS responses\n *\n * SECURITY NOTE: These templates return command strings that must be\n * validated against SAFE_ACTION_PATTERNS before execution.\n * Templates that accept user input are removed to prevent injection.\n */\nexport const ACTION_TEMPLATES = {\n // Git/PR actions (PR numbers must be validated as integers)\n approvePR: (prNumber: string) => {\n // Validate PR number is numeric only\n if (!/^\\d+$/.test(prNumber)) {\n throw new Error('Invalid PR number');\n }\n return `gh pr review ${prNumber} --approve`;\n },\n mergePR: (prNumber: string) => {\n if (!/^\\d+$/.test(prNumber)) {\n throw new Error('Invalid PR number');\n }\n return `gh pr merge ${prNumber} --squash`;\n },\n viewPR: (prNumber: string) => {\n if (!/^\\d+$/.test(prNumber)) {\n throw new Error('Invalid PR number');\n }\n return `gh pr view ${prNumber}`;\n },\n\n // Build actions (no user input)\n rebuild: () => `npm run build`,\n retest: () => `npm run test:run`,\n lint: () => `npm run lint:fix`,\n\n // Status actions (no user input)\n status: () => `stackmemory status`,\n checkNotifications: () => `stackmemory notify check`,\n\n // REMOVED for security - these templates allowed arbitrary user input:\n // - requestChanges (allowed arbitrary message)\n // - closePR (could be used maliciously)\n // - deploy/rollback (too dangerous for SMS)\n // - verifyDeployment (allowed arbitrary URL)\n // - notifySlack (allowed arbitrary message - command injection)\n // - notifyTeam (allowed arbitrary message - command injection)\n};\n\n/**\n * Create action string from template\n */\nexport function createAction(\n template: keyof typeof ACTION_TEMPLATES,\n ...args: string[]\n): string {\n const fn = ACTION_TEMPLATES[template];\n if (typeof fn === 'function') {\n return (fn as (...args: string[]) => string)(...args);\n }\n return fn;\n}\n\n/**\n * Watch for new actions and execute them\n */\nexport function startActionWatcher(intervalMs: number = 5000): NodeJS.Timeout {\n console.log(\n `[sms-action] Starting action watcher (interval: ${intervalMs}ms)`\n );\n\n return setInterval(() => {\n const pending = getPendingActions();\n if (pending.length > 0) {\n console.log(`[sms-action] Found ${pending.length} pending action(s)`);\n processAllPendingActions();\n }\n }, intervalMs);\n}\n\n/**\n * Integration with SMS webhook - queue action when response received\n */\nexport function handleSMSResponse(\n promptId: string,\n response: string,\n action?: string\n): void {\n if (action) {\n const actionId = queueAction(promptId, response, action);\n console.log(`[sms-action] Queued action ${actionId}: ${action}`);\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -5,6 +5,7 @@ const __dirname = __pathDirname(__filename);
|
|
|
5
5
|
import { existsSync, readFileSync } from "fs";
|
|
6
6
|
import { join } from "path";
|
|
7
7
|
import { homedir } from "os";
|
|
8
|
+
import { execFileSync } from "child_process";
|
|
8
9
|
import { writeFileSecure, ensureSecureDir } from "./secure-fs.js";
|
|
9
10
|
import { WhatsAppCommandsConfigSchema, parseConfigSafe } from "./schemas.js";
|
|
10
11
|
import { executeActionSafe } from "./sms-action-runner.js";
|
|
@@ -15,7 +16,60 @@ import {
|
|
|
15
16
|
loadSyncOptions
|
|
16
17
|
} from "./whatsapp-sync.js";
|
|
17
18
|
import { sendNotification } from "./sms-notify.js";
|
|
19
|
+
const REGEX_TIMEOUT_MS = 100;
|
|
20
|
+
const MAX_REGEX_INPUT_LENGTH = 200;
|
|
21
|
+
function safeRegexTest(pattern, input) {
|
|
22
|
+
const safeInput = input.slice(0, MAX_REGEX_INPUT_LENGTH);
|
|
23
|
+
try {
|
|
24
|
+
const regex = new RegExp(pattern);
|
|
25
|
+
const startTime = Date.now();
|
|
26
|
+
const result = regex.test(safeInput);
|
|
27
|
+
const elapsed = Date.now() - startTime;
|
|
28
|
+
if (elapsed > REGEX_TIMEOUT_MS) {
|
|
29
|
+
console.warn(
|
|
30
|
+
`[whatsapp-commands] Slow regex detected: ${pattern} took ${elapsed}ms`
|
|
31
|
+
);
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
return result;
|
|
35
|
+
} catch {
|
|
36
|
+
console.warn(`[whatsapp-commands] Invalid regex pattern: ${pattern}`);
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
18
40
|
const CONFIG_PATH = join(homedir(), ".stackmemory", "whatsapp-commands.json");
|
|
41
|
+
const REMOTE_SESSIONS_PATH = join(
|
|
42
|
+
homedir(),
|
|
43
|
+
".stackmemory",
|
|
44
|
+
"remote-sessions.json"
|
|
45
|
+
);
|
|
46
|
+
function loadRemoteSessions() {
|
|
47
|
+
try {
|
|
48
|
+
if (existsSync(REMOTE_SESSIONS_PATH)) {
|
|
49
|
+
return JSON.parse(readFileSync(REMOTE_SESSIONS_PATH, "utf8"));
|
|
50
|
+
}
|
|
51
|
+
} catch {
|
|
52
|
+
}
|
|
53
|
+
return { sessions: [] };
|
|
54
|
+
}
|
|
55
|
+
function saveRemoteSessions(store) {
|
|
56
|
+
try {
|
|
57
|
+
ensureSecureDir(join(homedir(), ".stackmemory"));
|
|
58
|
+
writeFileSecure(REMOTE_SESSIONS_PATH, JSON.stringify(store, null, 2));
|
|
59
|
+
} catch {
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
function addRemoteSession(session) {
|
|
63
|
+
const store = loadRemoteSessions();
|
|
64
|
+
store.sessions = [session, ...store.sessions.slice(0, 19)];
|
|
65
|
+
saveRemoteSessions(store);
|
|
66
|
+
}
|
|
67
|
+
function getRemoteSessions() {
|
|
68
|
+
return loadRemoteSessions().sessions;
|
|
69
|
+
}
|
|
70
|
+
function getActiveRemoteSessions() {
|
|
71
|
+
return loadRemoteSessions().sessions.filter((s) => s.status === "active");
|
|
72
|
+
}
|
|
19
73
|
const DEFAULT_COMMANDS = [
|
|
20
74
|
{
|
|
21
75
|
name: "status",
|
|
@@ -103,6 +157,19 @@ const DEFAULT_COMMANDS = [
|
|
|
103
157
|
description: "Show current branch",
|
|
104
158
|
enabled: true,
|
|
105
159
|
action: "git branch --show-current"
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
name: "remote",
|
|
163
|
+
description: "Launch remote Claude session (requires task prompt)",
|
|
164
|
+
enabled: true,
|
|
165
|
+
requiresArg: true
|
|
166
|
+
// No action - handled specially to capture session URL
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
name: "sessions",
|
|
170
|
+
description: "List active remote sessions",
|
|
171
|
+
enabled: true
|
|
172
|
+
// No action - handled specially
|
|
106
173
|
}
|
|
107
174
|
];
|
|
108
175
|
const DEFAULT_CONFIG = {
|
|
@@ -226,6 +293,63 @@ async function handleTasksCommand() {
|
|
|
226
293
|
return "Tasks unavailable";
|
|
227
294
|
}
|
|
228
295
|
}
|
|
296
|
+
async function handleRemoteCommand(prompt) {
|
|
297
|
+
try {
|
|
298
|
+
const sanitizedPrompt = prompt.replace(/[`$\\]/g, "").replace(/["']/g, "'").substring(0, 500);
|
|
299
|
+
if (!sanitizedPrompt.trim()) {
|
|
300
|
+
return "Please provide a task prompt. Usage: remote <your task>";
|
|
301
|
+
}
|
|
302
|
+
console.log(
|
|
303
|
+
`[whatsapp-commands] Launching remote session: ${sanitizedPrompt.substring(0, 50)}...`
|
|
304
|
+
);
|
|
305
|
+
const output = execFileSync("claude", ["--remote", sanitizedPrompt], {
|
|
306
|
+
encoding: "utf8",
|
|
307
|
+
timeout: 3e4,
|
|
308
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
309
|
+
});
|
|
310
|
+
const urlMatch = output.match(
|
|
311
|
+
/https:\/\/claude\.ai\/code\/session_[a-zA-Z0-9]+/
|
|
312
|
+
);
|
|
313
|
+
if (urlMatch) {
|
|
314
|
+
const sessionUrl = urlMatch[0];
|
|
315
|
+
const sessionId = sessionUrl.split("/").pop() || "unknown";
|
|
316
|
+
addRemoteSession({
|
|
317
|
+
id: sessionId,
|
|
318
|
+
url: sessionUrl,
|
|
319
|
+
prompt: sanitizedPrompt,
|
|
320
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
321
|
+
status: "active"
|
|
322
|
+
});
|
|
323
|
+
return `Remote session launched!
|
|
324
|
+
|
|
325
|
+
${sessionUrl}
|
|
326
|
+
|
|
327
|
+
Task: ${sanitizedPrompt.substring(0, 100)}`;
|
|
328
|
+
}
|
|
329
|
+
return `Session launched:
|
|
330
|
+
${output.substring(0, 300)}`;
|
|
331
|
+
} catch (err) {
|
|
332
|
+
const error = err instanceof Error ? err.message : String(err);
|
|
333
|
+
console.error(`[whatsapp-commands] Remote launch failed: ${error}`);
|
|
334
|
+
return `Failed to launch remote session: ${error.substring(0, 100)}`;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
function handleSessionsCommand() {
|
|
338
|
+
const sessions = getActiveRemoteSessions();
|
|
339
|
+
if (sessions.length === 0) {
|
|
340
|
+
return "No active remote sessions";
|
|
341
|
+
}
|
|
342
|
+
const lines = ["Active remote sessions:"];
|
|
343
|
+
sessions.slice(0, 5).forEach((s, i) => {
|
|
344
|
+
const age = Math.round(
|
|
345
|
+
(Date.now() - new Date(s.createdAt).getTime()) / 6e4
|
|
346
|
+
);
|
|
347
|
+
const ageStr = age < 60 ? `${age}m ago` : `${Math.round(age / 60)}h ago`;
|
|
348
|
+
lines.push(`${i + 1}. ${s.prompt.substring(0, 40)}... (${ageStr})`);
|
|
349
|
+
lines.push(` ${s.url}`);
|
|
350
|
+
});
|
|
351
|
+
return lines.join("\n");
|
|
352
|
+
}
|
|
229
353
|
async function processCommand(from, message) {
|
|
230
354
|
const config = loadCommandsConfig();
|
|
231
355
|
if (!config.enabled) {
|
|
@@ -261,6 +385,21 @@ async function processCommand(from, message) {
|
|
|
261
385
|
const tasksText = await handleTasksCommand();
|
|
262
386
|
return { handled: true, response: tasksText };
|
|
263
387
|
}
|
|
388
|
+
if (command.name === "remote") {
|
|
389
|
+
if (!parsed.arg) {
|
|
390
|
+
return {
|
|
391
|
+
handled: true,
|
|
392
|
+
response: "Usage: remote <task prompt>\nExample: remote Fix the login bug",
|
|
393
|
+
error: "Missing prompt"
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
const remoteText = await handleRemoteCommand(parsed.arg);
|
|
397
|
+
return { handled: true, response: remoteText };
|
|
398
|
+
}
|
|
399
|
+
if (command.name === "sessions") {
|
|
400
|
+
const sessionsText = handleSessionsCommand();
|
|
401
|
+
return { handled: true, response: sessionsText };
|
|
402
|
+
}
|
|
264
403
|
if (command.requiresArg && !parsed.arg) {
|
|
265
404
|
return {
|
|
266
405
|
handled: true,
|
|
@@ -269,8 +408,7 @@ async function processCommand(from, message) {
|
|
|
269
408
|
};
|
|
270
409
|
}
|
|
271
410
|
if (command.argPattern && parsed.arg) {
|
|
272
|
-
|
|
273
|
-
if (!pattern.test(parsed.arg)) {
|
|
411
|
+
if (!safeRegexTest(command.argPattern, parsed.arg)) {
|
|
274
412
|
return {
|
|
275
413
|
handled: true,
|
|
276
414
|
response: `Invalid argument format for ${command.name}`,
|
|
@@ -364,7 +502,9 @@ export {
|
|
|
364
502
|
addCommand,
|
|
365
503
|
disableCommands,
|
|
366
504
|
enableCommands,
|
|
505
|
+
getActiveRemoteSessions,
|
|
367
506
|
getAvailableCommands,
|
|
507
|
+
getRemoteSessions,
|
|
368
508
|
isCommand,
|
|
369
509
|
isCommandsEnabled,
|
|
370
510
|
loadCommandsConfig,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/hooks/whatsapp-commands.ts"],
|
|
4
|
-
"sourcesContent": ["/**\n * WhatsApp Inbound Command Processor\n * Process WhatsApp messages as commands\n */\n\nimport { existsSync, readFileSync } from 'fs';\nimport { join } from 'path';\nimport { homedir } from 'os';\nimport { writeFileSecure, ensureSecureDir } from './secure-fs.js';\nimport { WhatsAppCommandsConfigSchema, parseConfigSafe } from './schemas.js';\nimport { executeActionSafe } from './sms-action-runner.js';\nimport {\n syncContext,\n getFrameDigestData,\n generateMobileDigest,\n loadSyncOptions,\n} from './whatsapp-sync.js';\nimport { sendNotification } from './sms-notify.js';\n\nexport interface WhatsAppCommand {\n name: string;\n description: string;\n enabled: boolean;\n action?: string; // Safe action to execute\n requiresArg?: boolean;\n argPattern?: string; // Regex pattern for arg validation\n}\n\nexport interface CommandsConfig {\n enabled: boolean;\n commands: WhatsAppCommand[];\n}\n\nexport interface CommandResult {\n handled: boolean;\n response?: string;\n action?: string;\n error?: string;\n}\n\nconst CONFIG_PATH = join(homedir(), '.stackmemory', 'whatsapp-commands.json');\n\n// Default supported commands\nconst DEFAULT_COMMANDS: WhatsAppCommand[] = [\n {\n name: 'status',\n description: 'Get current task/frame status',\n enabled: true,\n // No action - handled specially in-process\n },\n {\n name: 'tasks',\n description: 'List active tasks',\n enabled: true,\n // No action - handled specially in-process\n },\n {\n name: 'context',\n description: 'Get latest context digest',\n enabled: true,\n // No action - handled specially\n },\n {\n name: 'approve',\n description: 'Approve a PR (requires PR number)',\n enabled: true,\n requiresArg: true,\n argPattern: '^\\\\d+$', // PR number must be numeric\n },\n {\n name: 'merge',\n description: 'Merge a PR (requires PR number)',\n enabled: true,\n requiresArg: true,\n argPattern: '^\\\\d+$',\n },\n {\n name: 'help',\n description: 'List available commands',\n enabled: true,\n // No action - handled specially\n },\n {\n name: 'sync',\n description: 'Push current context to WhatsApp',\n enabled: true,\n // No action - handled specially\n },\n {\n name: 'build',\n description: 'Run npm build',\n enabled: true,\n action: 'npm run build',\n },\n {\n name: 'test',\n description: 'Run tests',\n enabled: true,\n action: 'npm run test:run',\n },\n {\n name: 'lint',\n description: 'Run linter',\n enabled: true,\n action: 'npm run lint',\n },\n {\n name: 'log',\n description: 'Show recent git commits',\n enabled: true,\n action: 'git log --oneline -5',\n },\n {\n name: 'diff',\n description: 'Show git diff summary',\n enabled: true,\n action: 'git diff --stat',\n },\n {\n name: 'pr',\n description: 'List open PRs',\n enabled: true,\n action: 'gh pr list',\n },\n {\n name: 'branch',\n description: 'Show current branch',\n enabled: true,\n action: 'git branch --show-current',\n },\n];\n\nconst DEFAULT_CONFIG: CommandsConfig = {\n enabled: true,\n commands: DEFAULT_COMMANDS,\n};\n\n/**\n * Load commands config\n */\nexport function loadCommandsConfig(): CommandsConfig {\n try {\n if (existsSync(CONFIG_PATH)) {\n const data = JSON.parse(readFileSync(CONFIG_PATH, 'utf8'));\n return parseConfigSafe(\n WhatsAppCommandsConfigSchema,\n { ...DEFAULT_CONFIG, ...data },\n DEFAULT_CONFIG,\n 'whatsapp-commands'\n );\n }\n } catch {\n // Use defaults\n }\n return { ...DEFAULT_CONFIG };\n}\n\n/**\n * Save commands config\n */\nexport function saveCommandsConfig(config: CommandsConfig): void {\n try {\n ensureSecureDir(join(homedir(), '.stackmemory'));\n writeFileSecure(CONFIG_PATH, JSON.stringify(config, null, 2));\n } catch {\n // Silently fail\n }\n}\n\n/**\n * Check if a message is a command\n */\nexport function isCommand(message: string): boolean {\n const trimmed = message.trim().toLowerCase();\n\n // Check if it's a single word command\n const config = loadCommandsConfig();\n if (!config.enabled) return false;\n\n const words = trimmed.split(/\\s+/);\n const firstWord = words[0];\n\n return config.commands.some(\n (cmd) => cmd.enabled && cmd.name.toLowerCase() === firstWord\n );\n}\n\n/**\n * Parse command from message\n */\nfunction parseCommand(message: string): { name: string; arg?: string } | null {\n const trimmed = message.trim();\n const words = trimmed.split(/\\s+/);\n\n if (words.length === 0) return null;\n\n const name = words[0].toLowerCase();\n const arg = words.slice(1).join(' ').trim() || undefined;\n\n return { name, arg };\n}\n\n/**\n * Generate help text for available commands\n */\nfunction generateHelpText(config: CommandsConfig): string {\n const lines: string[] = ['Available commands:'];\n\n config.commands\n .filter((cmd) => cmd.enabled)\n .forEach((cmd) => {\n const argHint = cmd.requiresArg ? ' <arg>' : '';\n lines.push(` ${cmd.name}${argHint} - ${cmd.description}`);\n });\n\n lines.push('');\n lines.push('Reply with command name to execute');\n\n return lines.join('\\n');\n}\n\n/**\n * Handle the 'context' command specially\n */\nasync function handleContextCommand(): Promise<string> {\n const data = await getFrameDigestData();\n\n if (!data) {\n return 'No context available. Start a task first.';\n }\n\n const options = loadSyncOptions();\n return generateMobileDigest(data, options);\n}\n\n/**\n * Handle the 'sync' command specially\n */\nasync function handleSyncCommand(): Promise<string> {\n const result = await syncContext();\n\n if (result.success) {\n return `Context synced (${result.digestLength} chars)`;\n } else {\n return `Sync failed: ${result.error}`;\n }\n}\n\n/**\n * Handle the 'status' command - get current frame/task status\n */\nasync function handleStatusCommand(): Promise<string> {\n try {\n const data = await getFrameDigestData();\n if (!data) {\n return 'No active session. Start with: claude-sm';\n }\n\n const lines: string[] = [];\n lines.push(`Frame: ${data.name || data.frameId}`);\n lines.push(`Status: ${data.status}`);\n lines.push(`Files: ${data.filesModified?.length || 0} modified`);\n lines.push(`Tools: ${data.toolCallCount || 0} calls`);\n if (data.errors?.length > 0) {\n const unresolved = data.errors.filter((e) => !e.resolved).length;\n if (unresolved > 0) lines.push(`Errors: ${unresolved} unresolved`);\n }\n lines.push(`Duration: ${Math.round(data.durationSeconds / 60)}min`);\n\n return lines.join('\\n');\n } catch {\n return 'Status unavailable';\n }\n}\n\n/**\n * Handle the 'tasks' command - list recent decisions/risks\n */\nasync function handleTasksCommand(): Promise<string> {\n try {\n const data = await getFrameDigestData();\n if (!data) {\n return 'No active tasks';\n }\n\n const lines: string[] = [];\n\n if (data.decisions?.length > 0) {\n lines.push('Recent decisions:');\n data.decisions.slice(0, 3).forEach((d, i) => {\n lines.push(\n `${i + 1}. ${d.substring(0, 50)}${d.length > 50 ? '...' : ''}`\n );\n });\n }\n\n if (data.risks?.length > 0) {\n lines.push('');\n lines.push('Risks:');\n data.risks.slice(0, 2).forEach((r) => {\n lines.push(`- ${r.substring(0, 50)}${r.length > 50 ? '...' : ''}`);\n });\n }\n\n if (lines.length === 0) {\n return 'No active tasks or decisions';\n }\n\n return lines.join('\\n');\n } catch {\n return 'Tasks unavailable';\n }\n}\n\n/**\n * Process an incoming WhatsApp command\n */\nexport async function processCommand(\n from: string,\n message: string\n): Promise<CommandResult> {\n const config = loadCommandsConfig();\n\n if (!config.enabled) {\n return { handled: false };\n }\n\n const parsed = parseCommand(message);\n if (!parsed) {\n return { handled: false };\n }\n\n const command = config.commands.find(\n (cmd) => cmd.enabled && cmd.name.toLowerCase() === parsed.name\n );\n\n if (!command) {\n return { handled: false };\n }\n\n // Handle special commands\n if (command.name === 'help') {\n const helpText = generateHelpText(config);\n return { handled: true, response: helpText };\n }\n\n if (command.name === 'context') {\n const contextText = await handleContextCommand();\n return { handled: true, response: contextText };\n }\n\n if (command.name === 'sync') {\n const syncText = await handleSyncCommand();\n return { handled: true, response: syncText };\n }\n\n if (command.name === 'status') {\n const statusText = await handleStatusCommand();\n return { handled: true, response: statusText };\n }\n\n if (command.name === 'tasks') {\n const tasksText = await handleTasksCommand();\n return { handled: true, response: tasksText };\n }\n\n // Check if argument is required\n if (command.requiresArg && !parsed.arg) {\n return {\n handled: true,\n response: `${command.name} requires an argument. Usage: ${command.name} <arg>`,\n error: 'Missing argument',\n };\n }\n\n // Validate argument pattern if specified\n if (command.argPattern && parsed.arg) {\n const pattern = new RegExp(command.argPattern);\n if (!pattern.test(parsed.arg)) {\n return {\n handled: true,\n response: `Invalid argument format for ${command.name}`,\n error: 'Invalid argument format',\n };\n }\n }\n\n // Build the action command\n let action = command.action;\n\n if (action && parsed.arg) {\n // Special handling for PR commands\n if (command.name === 'approve') {\n action = `gh pr review ${parsed.arg} --approve`;\n } else if (command.name === 'merge') {\n action = `gh pr merge ${parsed.arg} --squash`;\n }\n }\n\n // Execute the action if defined\n if (action) {\n console.log(`[whatsapp-commands] Executing: ${action}`);\n\n const result = await executeActionSafe(action, message);\n\n if (result.success) {\n const output = result.output?.slice(0, 200) || 'Done';\n return {\n handled: true,\n response: `${command.name}: ${output}`,\n action,\n };\n } else {\n return {\n handled: true,\n response: `${command.name} failed: ${result.error?.slice(0, 100)}`,\n error: result.error,\n action,\n };\n }\n }\n\n return {\n handled: true,\n response: `Command ${command.name} acknowledged`,\n };\n}\n\n/**\n * Send command result back via WhatsApp\n */\nexport async function sendCommandResponse(\n response: string\n): Promise<{ success: boolean; error?: string }> {\n const result = await sendNotification({\n type: 'custom',\n title: 'Command Result',\n message: response,\n });\n\n return { success: result.success, error: result.error };\n}\n\n/**\n * Enable command processing\n */\nexport function enableCommands(): void {\n const config = loadCommandsConfig();\n config.enabled = true;\n saveCommandsConfig(config);\n}\n\n/**\n * Disable command processing\n */\nexport function disableCommands(): void {\n const config = loadCommandsConfig();\n config.enabled = false;\n saveCommandsConfig(config);\n}\n\n/**\n * Check if commands are enabled\n */\nexport function isCommandsEnabled(): boolean {\n const config = loadCommandsConfig();\n return config.enabled;\n}\n\n/**\n * Add a custom command\n */\nexport function addCommand(command: WhatsAppCommand): void {\n const config = loadCommandsConfig();\n\n // Check if command already exists\n const existingIndex = config.commands.findIndex(\n (c) => c.name.toLowerCase() === command.name.toLowerCase()\n );\n\n if (existingIndex >= 0) {\n config.commands[existingIndex] = command;\n } else {\n config.commands.push(command);\n }\n\n saveCommandsConfig(config);\n}\n\n/**\n * Remove a custom command\n */\nexport function removeCommand(name: string): boolean {\n const config = loadCommandsConfig();\n const initialLength = config.commands.length;\n\n config.commands = config.commands.filter(\n (c) => c.name.toLowerCase() !== name.toLowerCase()\n );\n\n if (config.commands.length < initialLength) {\n saveCommandsConfig(config);\n return true;\n }\n\n return false;\n}\n\n/**\n * Get list of available commands\n */\nexport function getAvailableCommands(): WhatsAppCommand[] {\n const config = loadCommandsConfig();\n return config.commands.filter((c) => c.enabled);\n}\n"],
|
|
5
|
-
"mappings": ";;;;AAKA,SAAS,YAAY,oBAAoB;AACzC,SAAS,YAAY;AACrB,SAAS,eAAe;AACxB,SAAS,iBAAiB,uBAAuB;AACjD,SAAS,8BAA8B,uBAAuB;AAC9D,SAAS,yBAAyB;AAClC;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,wBAAwB;
|
|
4
|
+
"sourcesContent": ["/**\n * WhatsApp Inbound Command Processor\n * Process WhatsApp messages as commands\n */\n\nimport { existsSync, readFileSync } from 'fs';\nimport { join } from 'path';\nimport { homedir } from 'os';\nimport { execFileSync } from 'child_process';\nimport { writeFileSecure, ensureSecureDir } from './secure-fs.js';\nimport { WhatsAppCommandsConfigSchema, parseConfigSafe } from './schemas.js';\nimport { executeActionSafe } from './sms-action-runner.js';\nimport {\n syncContext,\n getFrameDigestData,\n generateMobileDigest,\n loadSyncOptions,\n} from './whatsapp-sync.js';\nimport { sendNotification } from './sms-notify.js';\n\n// ReDoS protection: max execution time for regex test (ms)\nconst REGEX_TIMEOUT_MS = 100;\n// Max input length for regex matching to prevent catastrophic backtracking\nconst MAX_REGEX_INPUT_LENGTH = 200;\n\n/**\n * Safely test a regex pattern against input with ReDoS protection\n * Returns false if pattern is invalid, times out, or doesn't match\n */\nfunction safeRegexTest(pattern: string, input: string): boolean {\n // Truncate input to prevent catastrophic backtracking\n const safeInput = input.slice(0, MAX_REGEX_INPUT_LENGTH);\n\n try {\n const regex = new RegExp(pattern);\n\n // Use a simple timeout approach - run in try/catch with limited input\n // For true async timeout, we'd need Worker threads which adds complexity\n const startTime = Date.now();\n const result = regex.test(safeInput);\n const elapsed = Date.now() - startTime;\n\n // Log warning if regex took too long (could indicate ReDoS attempt)\n if (elapsed > REGEX_TIMEOUT_MS) {\n console.warn(\n `[whatsapp-commands] Slow regex detected: ${pattern} took ${elapsed}ms`\n );\n return false;\n }\n\n return result;\n } catch {\n // Invalid regex pattern\n console.warn(`[whatsapp-commands] Invalid regex pattern: ${pattern}`);\n return false;\n }\n}\n\nexport interface WhatsAppCommand {\n name: string;\n description: string;\n enabled: boolean;\n action?: string; // Safe action to execute\n requiresArg?: boolean;\n argPattern?: string; // Regex pattern for arg validation\n}\n\nexport interface CommandsConfig {\n enabled: boolean;\n commands: WhatsAppCommand[];\n}\n\nexport interface CommandResult {\n handled: boolean;\n response?: string;\n action?: string;\n error?: string;\n}\n\nconst CONFIG_PATH = join(homedir(), '.stackmemory', 'whatsapp-commands.json');\nconst REMOTE_SESSIONS_PATH = join(\n homedir(),\n '.stackmemory',\n 'remote-sessions.json'\n);\n\n/**\n * Remote session tracking\n */\nexport interface RemoteSession {\n id: string;\n url: string;\n prompt: string;\n createdAt: string;\n status: 'active' | 'completed' | 'failed';\n lastActivity?: string;\n}\n\ninterface RemoteSessionsStore {\n sessions: RemoteSession[];\n}\n\nfunction loadRemoteSessions(): RemoteSessionsStore {\n try {\n if (existsSync(REMOTE_SESSIONS_PATH)) {\n return JSON.parse(readFileSync(REMOTE_SESSIONS_PATH, 'utf8'));\n }\n } catch {\n // Use defaults\n }\n return { sessions: [] };\n}\n\nfunction saveRemoteSessions(store: RemoteSessionsStore): void {\n try {\n ensureSecureDir(join(homedir(), '.stackmemory'));\n writeFileSecure(REMOTE_SESSIONS_PATH, JSON.stringify(store, null, 2));\n } catch {\n // Silently fail\n }\n}\n\nfunction addRemoteSession(session: RemoteSession): void {\n const store = loadRemoteSessions();\n // Keep last 20 sessions\n store.sessions = [session, ...store.sessions.slice(0, 19)];\n saveRemoteSessions(store);\n}\n\nexport function getRemoteSessions(): RemoteSession[] {\n return loadRemoteSessions().sessions;\n}\n\nexport function getActiveRemoteSessions(): RemoteSession[] {\n return loadRemoteSessions().sessions.filter((s) => s.status === 'active');\n}\n\n// Default supported commands\nconst DEFAULT_COMMANDS: WhatsAppCommand[] = [\n {\n name: 'status',\n description: 'Get current task/frame status',\n enabled: true,\n // No action - handled specially in-process\n },\n {\n name: 'tasks',\n description: 'List active tasks',\n enabled: true,\n // No action - handled specially in-process\n },\n {\n name: 'context',\n description: 'Get latest context digest',\n enabled: true,\n // No action - handled specially\n },\n {\n name: 'approve',\n description: 'Approve a PR (requires PR number)',\n enabled: true,\n requiresArg: true,\n argPattern: '^\\\\d+$', // PR number must be numeric\n },\n {\n name: 'merge',\n description: 'Merge a PR (requires PR number)',\n enabled: true,\n requiresArg: true,\n argPattern: '^\\\\d+$',\n },\n {\n name: 'help',\n description: 'List available commands',\n enabled: true,\n // No action - handled specially\n },\n {\n name: 'sync',\n description: 'Push current context to WhatsApp',\n enabled: true,\n // No action - handled specially\n },\n {\n name: 'build',\n description: 'Run npm build',\n enabled: true,\n action: 'npm run build',\n },\n {\n name: 'test',\n description: 'Run tests',\n enabled: true,\n action: 'npm run test:run',\n },\n {\n name: 'lint',\n description: 'Run linter',\n enabled: true,\n action: 'npm run lint',\n },\n {\n name: 'log',\n description: 'Show recent git commits',\n enabled: true,\n action: 'git log --oneline -5',\n },\n {\n name: 'diff',\n description: 'Show git diff summary',\n enabled: true,\n action: 'git diff --stat',\n },\n {\n name: 'pr',\n description: 'List open PRs',\n enabled: true,\n action: 'gh pr list',\n },\n {\n name: 'branch',\n description: 'Show current branch',\n enabled: true,\n action: 'git branch --show-current',\n },\n {\n name: 'remote',\n description: 'Launch remote Claude session (requires task prompt)',\n enabled: true,\n requiresArg: true,\n // No action - handled specially to capture session URL\n },\n {\n name: 'sessions',\n description: 'List active remote sessions',\n enabled: true,\n // No action - handled specially\n },\n];\n\nconst DEFAULT_CONFIG: CommandsConfig = {\n enabled: true,\n commands: DEFAULT_COMMANDS,\n};\n\n/**\n * Load commands config\n */\nexport function loadCommandsConfig(): CommandsConfig {\n try {\n if (existsSync(CONFIG_PATH)) {\n const data = JSON.parse(readFileSync(CONFIG_PATH, 'utf8'));\n return parseConfigSafe(\n WhatsAppCommandsConfigSchema,\n { ...DEFAULT_CONFIG, ...data },\n DEFAULT_CONFIG,\n 'whatsapp-commands'\n );\n }\n } catch {\n // Use defaults\n }\n return { ...DEFAULT_CONFIG };\n}\n\n/**\n * Save commands config\n */\nexport function saveCommandsConfig(config: CommandsConfig): void {\n try {\n ensureSecureDir(join(homedir(), '.stackmemory'));\n writeFileSecure(CONFIG_PATH, JSON.stringify(config, null, 2));\n } catch {\n // Silently fail\n }\n}\n\n/**\n * Check if a message is a command\n */\nexport function isCommand(message: string): boolean {\n const trimmed = message.trim().toLowerCase();\n\n // Check if it's a single word command\n const config = loadCommandsConfig();\n if (!config.enabled) return false;\n\n const words = trimmed.split(/\\s+/);\n const firstWord = words[0];\n\n return config.commands.some(\n (cmd) => cmd.enabled && cmd.name.toLowerCase() === firstWord\n );\n}\n\n/**\n * Parse command from message\n */\nfunction parseCommand(message: string): { name: string; arg?: string } | null {\n const trimmed = message.trim();\n const words = trimmed.split(/\\s+/);\n\n if (words.length === 0) return null;\n\n const name = words[0].toLowerCase();\n const arg = words.slice(1).join(' ').trim() || undefined;\n\n return { name, arg };\n}\n\n/**\n * Generate help text for available commands\n */\nfunction generateHelpText(config: CommandsConfig): string {\n const lines: string[] = ['Available commands:'];\n\n config.commands\n .filter((cmd) => cmd.enabled)\n .forEach((cmd) => {\n const argHint = cmd.requiresArg ? ' <arg>' : '';\n lines.push(` ${cmd.name}${argHint} - ${cmd.description}`);\n });\n\n lines.push('');\n lines.push('Reply with command name to execute');\n\n return lines.join('\\n');\n}\n\n/**\n * Handle the 'context' command specially\n */\nasync function handleContextCommand(): Promise<string> {\n const data = await getFrameDigestData();\n\n if (!data) {\n return 'No context available. Start a task first.';\n }\n\n const options = loadSyncOptions();\n return generateMobileDigest(data, options);\n}\n\n/**\n * Handle the 'sync' command specially\n */\nasync function handleSyncCommand(): Promise<string> {\n const result = await syncContext();\n\n if (result.success) {\n return `Context synced (${result.digestLength} chars)`;\n } else {\n return `Sync failed: ${result.error}`;\n }\n}\n\n/**\n * Handle the 'status' command - get current frame/task status\n */\nasync function handleStatusCommand(): Promise<string> {\n try {\n const data = await getFrameDigestData();\n if (!data) {\n return 'No active session. Start with: claude-sm';\n }\n\n const lines: string[] = [];\n lines.push(`Frame: ${data.name || data.frameId}`);\n lines.push(`Status: ${data.status}`);\n lines.push(`Files: ${data.filesModified?.length || 0} modified`);\n lines.push(`Tools: ${data.toolCallCount || 0} calls`);\n if (data.errors?.length > 0) {\n const unresolved = data.errors.filter((e) => !e.resolved).length;\n if (unresolved > 0) lines.push(`Errors: ${unresolved} unresolved`);\n }\n lines.push(`Duration: ${Math.round(data.durationSeconds / 60)}min`);\n\n return lines.join('\\n');\n } catch {\n return 'Status unavailable';\n }\n}\n\n/**\n * Handle the 'tasks' command - list recent decisions/risks\n */\nasync function handleTasksCommand(): Promise<string> {\n try {\n const data = await getFrameDigestData();\n if (!data) {\n return 'No active tasks';\n }\n\n const lines: string[] = [];\n\n if (data.decisions?.length > 0) {\n lines.push('Recent decisions:');\n data.decisions.slice(0, 3).forEach((d, i) => {\n lines.push(\n `${i + 1}. ${d.substring(0, 50)}${d.length > 50 ? '...' : ''}`\n );\n });\n }\n\n if (data.risks?.length > 0) {\n lines.push('');\n lines.push('Risks:');\n data.risks.slice(0, 2).forEach((r) => {\n lines.push(`- ${r.substring(0, 50)}${r.length > 50 ? '...' : ''}`);\n });\n }\n\n if (lines.length === 0) {\n return 'No active tasks or decisions';\n }\n\n return lines.join('\\n');\n } catch {\n return 'Tasks unavailable';\n }\n}\n\n/**\n * Handle the 'remote' command - launch a remote Claude session\n */\nasync function handleRemoteCommand(prompt: string): Promise<string> {\n try {\n // Sanitize prompt - remove any shell-dangerous characters\n const sanitizedPrompt = prompt\n .replace(/[`$\\\\]/g, '')\n .replace(/[\"']/g, \"'\")\n .substring(0, 500);\n\n if (!sanitizedPrompt.trim()) {\n return 'Please provide a task prompt. Usage: remote <your task>';\n }\n\n console.log(\n `[whatsapp-commands] Launching remote session: ${sanitizedPrompt.substring(0, 50)}...`\n );\n\n // Execute claude --remote with the prompt\n const output = execFileSync('claude', ['--remote', sanitizedPrompt], {\n encoding: 'utf8',\n timeout: 30000,\n stdio: ['pipe', 'pipe', 'pipe'],\n });\n\n // Parse session URL from output\n // Expected format contains: https://claude.ai/code/session_...\n const urlMatch = output.match(\n /https:\\/\\/claude\\.ai\\/code\\/session_[a-zA-Z0-9]+/\n );\n\n if (urlMatch) {\n const sessionUrl = urlMatch[0];\n const sessionId = sessionUrl.split('/').pop() || 'unknown';\n\n // Store the session\n addRemoteSession({\n id: sessionId,\n url: sessionUrl,\n prompt: sanitizedPrompt,\n createdAt: new Date().toISOString(),\n status: 'active',\n });\n\n return `Remote session launched!\\n\\n${sessionUrl}\\n\\nTask: ${sanitizedPrompt.substring(0, 100)}`;\n }\n\n // No URL found - return raw output\n return `Session launched:\\n${output.substring(0, 300)}`;\n } catch (err) {\n const error = err instanceof Error ? err.message : String(err);\n console.error(`[whatsapp-commands] Remote launch failed: ${error}`);\n return `Failed to launch remote session: ${error.substring(0, 100)}`;\n }\n}\n\n/**\n * Handle the 'sessions' command - list active remote sessions\n */\nfunction handleSessionsCommand(): string {\n const sessions = getActiveRemoteSessions();\n\n if (sessions.length === 0) {\n return 'No active remote sessions';\n }\n\n const lines: string[] = ['Active remote sessions:'];\n\n sessions.slice(0, 5).forEach((s, i) => {\n const age = Math.round(\n (Date.now() - new Date(s.createdAt).getTime()) / 60000\n );\n const ageStr = age < 60 ? `${age}m ago` : `${Math.round(age / 60)}h ago`;\n lines.push(`${i + 1}. ${s.prompt.substring(0, 40)}... (${ageStr})`);\n lines.push(` ${s.url}`);\n });\n\n return lines.join('\\n');\n}\n\n/**\n * Process an incoming WhatsApp command\n */\nexport async function processCommand(\n from: string,\n message: string\n): Promise<CommandResult> {\n const config = loadCommandsConfig();\n\n if (!config.enabled) {\n return { handled: false };\n }\n\n const parsed = parseCommand(message);\n if (!parsed) {\n return { handled: false };\n }\n\n const command = config.commands.find(\n (cmd) => cmd.enabled && cmd.name.toLowerCase() === parsed.name\n );\n\n if (!command) {\n return { handled: false };\n }\n\n // Handle special commands\n if (command.name === 'help') {\n const helpText = generateHelpText(config);\n return { handled: true, response: helpText };\n }\n\n if (command.name === 'context') {\n const contextText = await handleContextCommand();\n return { handled: true, response: contextText };\n }\n\n if (command.name === 'sync') {\n const syncText = await handleSyncCommand();\n return { handled: true, response: syncText };\n }\n\n if (command.name === 'status') {\n const statusText = await handleStatusCommand();\n return { handled: true, response: statusText };\n }\n\n if (command.name === 'tasks') {\n const tasksText = await handleTasksCommand();\n return { handled: true, response: tasksText };\n }\n\n if (command.name === 'remote') {\n if (!parsed.arg) {\n return {\n handled: true,\n response:\n 'Usage: remote <task prompt>\\nExample: remote Fix the login bug',\n error: 'Missing prompt',\n };\n }\n const remoteText = await handleRemoteCommand(parsed.arg);\n return { handled: true, response: remoteText };\n }\n\n if (command.name === 'sessions') {\n const sessionsText = handleSessionsCommand();\n return { handled: true, response: sessionsText };\n }\n\n // Check if argument is required\n if (command.requiresArg && !parsed.arg) {\n return {\n handled: true,\n response: `${command.name} requires an argument. Usage: ${command.name} <arg>`,\n error: 'Missing argument',\n };\n }\n\n // Validate argument pattern if specified (with ReDoS protection)\n if (command.argPattern && parsed.arg) {\n if (!safeRegexTest(command.argPattern, parsed.arg)) {\n return {\n handled: true,\n response: `Invalid argument format for ${command.name}`,\n error: 'Invalid argument format',\n };\n }\n }\n\n // Build the action command\n let action = command.action;\n\n if (action && parsed.arg) {\n // Special handling for PR commands\n if (command.name === 'approve') {\n action = `gh pr review ${parsed.arg} --approve`;\n } else if (command.name === 'merge') {\n action = `gh pr merge ${parsed.arg} --squash`;\n }\n }\n\n // Execute the action if defined\n if (action) {\n console.log(`[whatsapp-commands] Executing: ${action}`);\n\n const result = await executeActionSafe(action, message);\n\n if (result.success) {\n const output = result.output?.slice(0, 200) || 'Done';\n return {\n handled: true,\n response: `${command.name}: ${output}`,\n action,\n };\n } else {\n return {\n handled: true,\n response: `${command.name} failed: ${result.error?.slice(0, 100)}`,\n error: result.error,\n action,\n };\n }\n }\n\n return {\n handled: true,\n response: `Command ${command.name} acknowledged`,\n };\n}\n\n/**\n * Send command result back via WhatsApp\n */\nexport async function sendCommandResponse(\n response: string\n): Promise<{ success: boolean; error?: string }> {\n const result = await sendNotification({\n type: 'custom',\n title: 'Command Result',\n message: response,\n });\n\n return { success: result.success, error: result.error };\n}\n\n/**\n * Enable command processing\n */\nexport function enableCommands(): void {\n const config = loadCommandsConfig();\n config.enabled = true;\n saveCommandsConfig(config);\n}\n\n/**\n * Disable command processing\n */\nexport function disableCommands(): void {\n const config = loadCommandsConfig();\n config.enabled = false;\n saveCommandsConfig(config);\n}\n\n/**\n * Check if commands are enabled\n */\nexport function isCommandsEnabled(): boolean {\n const config = loadCommandsConfig();\n return config.enabled;\n}\n\n/**\n * Add a custom command\n */\nexport function addCommand(command: WhatsAppCommand): void {\n const config = loadCommandsConfig();\n\n // Check if command already exists\n const existingIndex = config.commands.findIndex(\n (c) => c.name.toLowerCase() === command.name.toLowerCase()\n );\n\n if (existingIndex >= 0) {\n config.commands[existingIndex] = command;\n } else {\n config.commands.push(command);\n }\n\n saveCommandsConfig(config);\n}\n\n/**\n * Remove a custom command\n */\nexport function removeCommand(name: string): boolean {\n const config = loadCommandsConfig();\n const initialLength = config.commands.length;\n\n config.commands = config.commands.filter(\n (c) => c.name.toLowerCase() !== name.toLowerCase()\n );\n\n if (config.commands.length < initialLength) {\n saveCommandsConfig(config);\n return true;\n }\n\n return false;\n}\n\n/**\n * Get list of available commands\n */\nexport function getAvailableCommands(): WhatsAppCommand[] {\n const config = loadCommandsConfig();\n return config.commands.filter((c) => c.enabled);\n}\n"],
|
|
5
|
+
"mappings": ";;;;AAKA,SAAS,YAAY,oBAAoB;AACzC,SAAS,YAAY;AACrB,SAAS,eAAe;AACxB,SAAS,oBAAoB;AAC7B,SAAS,iBAAiB,uBAAuB;AACjD,SAAS,8BAA8B,uBAAuB;AAC9D,SAAS,yBAAyB;AAClC;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,wBAAwB;AAGjC,MAAM,mBAAmB;AAEzB,MAAM,yBAAyB;AAM/B,SAAS,cAAc,SAAiB,OAAwB;AAE9D,QAAM,YAAY,MAAM,MAAM,GAAG,sBAAsB;AAEvD,MAAI;AACF,UAAM,QAAQ,IAAI,OAAO,OAAO;AAIhC,UAAM,YAAY,KAAK,IAAI;AAC3B,UAAM,SAAS,MAAM,KAAK,SAAS;AACnC,UAAM,UAAU,KAAK,IAAI,IAAI;AAG7B,QAAI,UAAU,kBAAkB;AAC9B,cAAQ;AAAA,QACN,4CAA4C,OAAO,SAAS,OAAO;AAAA,MACrE;AACA,aAAO;AAAA,IACT;AAEA,WAAO;AAAA,EACT,QAAQ;AAEN,YAAQ,KAAK,8CAA8C,OAAO,EAAE;AACpE,WAAO;AAAA,EACT;AACF;AAuBA,MAAM,cAAc,KAAK,QAAQ,GAAG,gBAAgB,wBAAwB;AAC5E,MAAM,uBAAuB;AAAA,EAC3B,QAAQ;AAAA,EACR;AAAA,EACA;AACF;AAkBA,SAAS,qBAA0C;AACjD,MAAI;AACF,QAAI,WAAW,oBAAoB,GAAG;AACpC,aAAO,KAAK,MAAM,aAAa,sBAAsB,MAAM,CAAC;AAAA,IAC9D;AAAA,EACF,QAAQ;AAAA,EAER;AACA,SAAO,EAAE,UAAU,CAAC,EAAE;AACxB;AAEA,SAAS,mBAAmB,OAAkC;AAC5D,MAAI;AACF,oBAAgB,KAAK,QAAQ,GAAG,cAAc,CAAC;AAC/C,oBAAgB,sBAAsB,KAAK,UAAU,OAAO,MAAM,CAAC,CAAC;AAAA,EACtE,QAAQ;AAAA,EAER;AACF;AAEA,SAAS,iBAAiB,SAA8B;AACtD,QAAM,QAAQ,mBAAmB;AAEjC,QAAM,WAAW,CAAC,SAAS,GAAG,MAAM,SAAS,MAAM,GAAG,EAAE,CAAC;AACzD,qBAAmB,KAAK;AAC1B;AAEO,SAAS,oBAAqC;AACnD,SAAO,mBAAmB,EAAE;AAC9B;AAEO,SAAS,0BAA2C;AACzD,SAAO,mBAAmB,EAAE,SAAS,OAAO,CAAC,MAAM,EAAE,WAAW,QAAQ;AAC1E;AAGA,MAAM,mBAAsC;AAAA,EAC1C;AAAA,IACE,MAAM;AAAA,IACN,aAAa;AAAA,IACb,SAAS;AAAA;AAAA,EAEX;AAAA,EACA;AAAA,IACE,MAAM;AAAA,IACN,aAAa;AAAA,IACb,SAAS;AAAA;AAAA,EAEX;AAAA,EACA;AAAA,IACE,MAAM;AAAA,IACN,aAAa;AAAA,IACb,SAAS;AAAA;AAAA,EAEX;AAAA,EACA;AAAA,IACE,MAAM;AAAA,IACN,aAAa;AAAA,IACb,SAAS;AAAA,IACT,aAAa;AAAA,IACb,YAAY;AAAA;AAAA,EACd;AAAA,EACA;AAAA,IACE,MAAM;AAAA,IACN,aAAa;AAAA,IACb,SAAS;AAAA,IACT,aAAa;AAAA,IACb,YAAY;AAAA,EACd;AAAA,EACA;AAAA,IACE,MAAM;AAAA,IACN,aAAa;AAAA,IACb,SAAS;AAAA;AAAA,EAEX;AAAA,EACA;AAAA,IACE,MAAM;AAAA,IACN,aAAa;AAAA,IACb,SAAS;AAAA;AAAA,EAEX;AAAA,EACA;AAAA,IACE,MAAM;AAAA,IACN,aAAa;AAAA,IACb,SAAS;AAAA,IACT,QAAQ;AAAA,EACV;AAAA,EACA;AAAA,IACE,MAAM;AAAA,IACN,aAAa;AAAA,IACb,SAAS;AAAA,IACT,QAAQ;AAAA,EACV;AAAA,EACA;AAAA,IACE,MAAM;AAAA,IACN,aAAa;AAAA,IACb,SAAS;AAAA,IACT,QAAQ;AAAA,EACV;AAAA,EACA;AAAA,IACE,MAAM;AAAA,IACN,aAAa;AAAA,IACb,SAAS;AAAA,IACT,QAAQ;AAAA,EACV;AAAA,EACA;AAAA,IACE,MAAM;AAAA,IACN,aAAa;AAAA,IACb,SAAS;AAAA,IACT,QAAQ;AAAA,EACV;AAAA,EACA;AAAA,IACE,MAAM;AAAA,IACN,aAAa;AAAA,IACb,SAAS;AAAA,IACT,QAAQ;AAAA,EACV;AAAA,EACA;AAAA,IACE,MAAM;AAAA,IACN,aAAa;AAAA,IACb,SAAS;AAAA,IACT,QAAQ;AAAA,EACV;AAAA,EACA;AAAA,IACE,MAAM;AAAA,IACN,aAAa;AAAA,IACb,SAAS;AAAA,IACT,aAAa;AAAA;AAAA,EAEf;AAAA,EACA;AAAA,IACE,MAAM;AAAA,IACN,aAAa;AAAA,IACb,SAAS;AAAA;AAAA,EAEX;AACF;AAEA,MAAM,iBAAiC;AAAA,EACrC,SAAS;AAAA,EACT,UAAU;AACZ;AAKO,SAAS,qBAAqC;AACnD,MAAI;AACF,QAAI,WAAW,WAAW,GAAG;AAC3B,YAAM,OAAO,KAAK,MAAM,aAAa,aAAa,MAAM,CAAC;AACzD,aAAO;AAAA,QACL;AAAA,QACA,EAAE,GAAG,gBAAgB,GAAG,KAAK;AAAA,QAC7B;AAAA,QACA;AAAA,MACF;AAAA,IACF;AAAA,EACF,QAAQ;AAAA,EAER;AACA,SAAO,EAAE,GAAG,eAAe;AAC7B;AAKO,SAAS,mBAAmB,QAA8B;AAC/D,MAAI;AACF,oBAAgB,KAAK,QAAQ,GAAG,cAAc,CAAC;AAC/C,oBAAgB,aAAa,KAAK,UAAU,QAAQ,MAAM,CAAC,CAAC;AAAA,EAC9D,QAAQ;AAAA,EAER;AACF;AAKO,SAAS,UAAU,SAA0B;AAClD,QAAM,UAAU,QAAQ,KAAK,EAAE,YAAY;AAG3C,QAAM,SAAS,mBAAmB;AAClC,MAAI,CAAC,OAAO,QAAS,QAAO;AAE5B,QAAM,QAAQ,QAAQ,MAAM,KAAK;AACjC,QAAM,YAAY,MAAM,CAAC;AAEzB,SAAO,OAAO,SAAS;AAAA,IACrB,CAAC,QAAQ,IAAI,WAAW,IAAI,KAAK,YAAY,MAAM;AAAA,EACrD;AACF;AAKA,SAAS,aAAa,SAAwD;AAC5E,QAAM,UAAU,QAAQ,KAAK;AAC7B,QAAM,QAAQ,QAAQ,MAAM,KAAK;AAEjC,MAAI,MAAM,WAAW,EAAG,QAAO;AAE/B,QAAM,OAAO,MAAM,CAAC,EAAE,YAAY;AAClC,QAAM,MAAM,MAAM,MAAM,CAAC,EAAE,KAAK,GAAG,EAAE,KAAK,KAAK;AAE/C,SAAO,EAAE,MAAM,IAAI;AACrB;AAKA,SAAS,iBAAiB,QAAgC;AACxD,QAAM,QAAkB,CAAC,qBAAqB;AAE9C,SAAO,SACJ,OAAO,CAAC,QAAQ,IAAI,OAAO,EAC3B,QAAQ,CAAC,QAAQ;AAChB,UAAM,UAAU,IAAI,cAAc,WAAW;AAC7C,UAAM,KAAK,KAAK,IAAI,IAAI,GAAG,OAAO,MAAM,IAAI,WAAW,EAAE;AAAA,EAC3D,CAAC;AAEH,QAAM,KAAK,EAAE;AACb,QAAM,KAAK,oCAAoC;AAE/C,SAAO,MAAM,KAAK,IAAI;AACxB;AAKA,eAAe,uBAAwC;AACrD,QAAM,OAAO,MAAM,mBAAmB;AAEtC,MAAI,CAAC,MAAM;AACT,WAAO;AAAA,EACT;AAEA,QAAM,UAAU,gBAAgB;AAChC,SAAO,qBAAqB,MAAM,OAAO;AAC3C;AAKA,eAAe,oBAAqC;AAClD,QAAM,SAAS,MAAM,YAAY;AAEjC,MAAI,OAAO,SAAS;AAClB,WAAO,mBAAmB,OAAO,YAAY;AAAA,EAC/C,OAAO;AACL,WAAO,gBAAgB,OAAO,KAAK;AAAA,EACrC;AACF;AAKA,eAAe,sBAAuC;AACpD,MAAI;AACF,UAAM,OAAO,MAAM,mBAAmB;AACtC,QAAI,CAAC,MAAM;AACT,aAAO;AAAA,IACT;AAEA,UAAM,QAAkB,CAAC;AACzB,UAAM,KAAK,UAAU,KAAK,QAAQ,KAAK,OAAO,EAAE;AAChD,UAAM,KAAK,WAAW,KAAK,MAAM,EAAE;AACnC,UAAM,KAAK,UAAU,KAAK,eAAe,UAAU,CAAC,WAAW;AAC/D,UAAM,KAAK,UAAU,KAAK,iBAAiB,CAAC,QAAQ;AACpD,QAAI,KAAK,QAAQ,SAAS,GAAG;AAC3B,YAAM,aAAa,KAAK,OAAO,OAAO,CAAC,MAAM,CAAC,EAAE,QAAQ,EAAE;AAC1D,UAAI,aAAa,EAAG,OAAM,KAAK,WAAW,UAAU,aAAa;AAAA,IACnE;AACA,UAAM,KAAK,aAAa,KAAK,MAAM,KAAK,kBAAkB,EAAE,CAAC,KAAK;AAElE,WAAO,MAAM,KAAK,IAAI;AAAA,EACxB,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAKA,eAAe,qBAAsC;AACnD,MAAI;AACF,UAAM,OAAO,MAAM,mBAAmB;AACtC,QAAI,CAAC,MAAM;AACT,aAAO;AAAA,IACT;AAEA,UAAM,QAAkB,CAAC;AAEzB,QAAI,KAAK,WAAW,SAAS,GAAG;AAC9B,YAAM,KAAK,mBAAmB;AAC9B,WAAK,UAAU,MAAM,GAAG,CAAC,EAAE,QAAQ,CAAC,GAAG,MAAM;AAC3C,cAAM;AAAA,UACJ,GAAG,IAAI,CAAC,KAAK,EAAE,UAAU,GAAG,EAAE,CAAC,GAAG,EAAE,SAAS,KAAK,QAAQ,EAAE;AAAA,QAC9D;AAAA,MACF,CAAC;AAAA,IACH;AAEA,QAAI,KAAK,OAAO,SAAS,GAAG;AAC1B,YAAM,KAAK,EAAE;AACb,YAAM,KAAK,QAAQ;AACnB,WAAK,MAAM,MAAM,GAAG,CAAC,EAAE,QAAQ,CAAC,MAAM;AACpC,cAAM,KAAK,KAAK,EAAE,UAAU,GAAG,EAAE,CAAC,GAAG,EAAE,SAAS,KAAK,QAAQ,EAAE,EAAE;AAAA,MACnE,CAAC;AAAA,IACH;AAEA,QAAI,MAAM,WAAW,GAAG;AACtB,aAAO;AAAA,IACT;AAEA,WAAO,MAAM,KAAK,IAAI;AAAA,EACxB,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAKA,eAAe,oBAAoB,QAAiC;AAClE,MAAI;AAEF,UAAM,kBAAkB,OACrB,QAAQ,WAAW,EAAE,EACrB,QAAQ,SAAS,GAAG,EACpB,UAAU,GAAG,GAAG;AAEnB,QAAI,CAAC,gBAAgB,KAAK,GAAG;AAC3B,aAAO;AAAA,IACT;AAEA,YAAQ;AAAA,MACN,iDAAiD,gBAAgB,UAAU,GAAG,EAAE,CAAC;AAAA,IACnF;AAGA,UAAM,SAAS,aAAa,UAAU,CAAC,YAAY,eAAe,GAAG;AAAA,MACnE,UAAU;AAAA,MACV,SAAS;AAAA,MACT,OAAO,CAAC,QAAQ,QAAQ,MAAM;AAAA,IAChC,CAAC;AAID,UAAM,WAAW,OAAO;AAAA,MACtB;AAAA,IACF;AAEA,QAAI,UAAU;AACZ,YAAM,aAAa,SAAS,CAAC;AAC7B,YAAM,YAAY,WAAW,MAAM,GAAG,EAAE,IAAI,KAAK;AAGjD,uBAAiB;AAAA,QACf,IAAI;AAAA,QACJ,KAAK;AAAA,QACL,QAAQ;AAAA,QACR,YAAW,oBAAI,KAAK,GAAE,YAAY;AAAA,QAClC,QAAQ;AAAA,MACV,CAAC;AAED,aAAO;AAAA;AAAA,EAA+B,UAAU;AAAA;AAAA,QAAa,gBAAgB,UAAU,GAAG,GAAG,CAAC;AAAA,IAChG;AAGA,WAAO;AAAA,EAAsB,OAAO,UAAU,GAAG,GAAG,CAAC;AAAA,EACvD,SAAS,KAAK;AACZ,UAAM,QAAQ,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;AAC7D,YAAQ,MAAM,6CAA6C,KAAK,EAAE;AAClE,WAAO,oCAAoC,MAAM,UAAU,GAAG,GAAG,CAAC;AAAA,EACpE;AACF;AAKA,SAAS,wBAAgC;AACvC,QAAM,WAAW,wBAAwB;AAEzC,MAAI,SAAS,WAAW,GAAG;AACzB,WAAO;AAAA,EACT;AAEA,QAAM,QAAkB,CAAC,yBAAyB;AAElD,WAAS,MAAM,GAAG,CAAC,EAAE,QAAQ,CAAC,GAAG,MAAM;AACrC,UAAM,MAAM,KAAK;AAAA,OACd,KAAK,IAAI,IAAI,IAAI,KAAK,EAAE,SAAS,EAAE,QAAQ,KAAK;AAAA,IACnD;AACA,UAAM,SAAS,MAAM,KAAK,GAAG,GAAG,UAAU,GAAG,KAAK,MAAM,MAAM,EAAE,CAAC;AACjE,UAAM,KAAK,GAAG,IAAI,CAAC,KAAK,EAAE,OAAO,UAAU,GAAG,EAAE,CAAC,QAAQ,MAAM,GAAG;AAClE,UAAM,KAAK,MAAM,EAAE,GAAG,EAAE;AAAA,EAC1B,CAAC;AAED,SAAO,MAAM,KAAK,IAAI;AACxB;AAKA,eAAsB,eACpB,MACA,SACwB;AACxB,QAAM,SAAS,mBAAmB;AAElC,MAAI,CAAC,OAAO,SAAS;AACnB,WAAO,EAAE,SAAS,MAAM;AAAA,EAC1B;AAEA,QAAM,SAAS,aAAa,OAAO;AACnC,MAAI,CAAC,QAAQ;AACX,WAAO,EAAE,SAAS,MAAM;AAAA,EAC1B;AAEA,QAAM,UAAU,OAAO,SAAS;AAAA,IAC9B,CAAC,QAAQ,IAAI,WAAW,IAAI,KAAK,YAAY,MAAM,OAAO;AAAA,EAC5D;AAEA,MAAI,CAAC,SAAS;AACZ,WAAO,EAAE,SAAS,MAAM;AAAA,EAC1B;AAGA,MAAI,QAAQ,SAAS,QAAQ;AAC3B,UAAM,WAAW,iBAAiB,MAAM;AACxC,WAAO,EAAE,SAAS,MAAM,UAAU,SAAS;AAAA,EAC7C;AAEA,MAAI,QAAQ,SAAS,WAAW;AAC9B,UAAM,cAAc,MAAM,qBAAqB;AAC/C,WAAO,EAAE,SAAS,MAAM,UAAU,YAAY;AAAA,EAChD;AAEA,MAAI,QAAQ,SAAS,QAAQ;AAC3B,UAAM,WAAW,MAAM,kBAAkB;AACzC,WAAO,EAAE,SAAS,MAAM,UAAU,SAAS;AAAA,EAC7C;AAEA,MAAI,QAAQ,SAAS,UAAU;AAC7B,UAAM,aAAa,MAAM,oBAAoB;AAC7C,WAAO,EAAE,SAAS,MAAM,UAAU,WAAW;AAAA,EAC/C;AAEA,MAAI,QAAQ,SAAS,SAAS;AAC5B,UAAM,YAAY,MAAM,mBAAmB;AAC3C,WAAO,EAAE,SAAS,MAAM,UAAU,UAAU;AAAA,EAC9C;AAEA,MAAI,QAAQ,SAAS,UAAU;AAC7B,QAAI,CAAC,OAAO,KAAK;AACf,aAAO;AAAA,QACL,SAAS;AAAA,QACT,UACE;AAAA,QACF,OAAO;AAAA,MACT;AAAA,IACF;AACA,UAAM,aAAa,MAAM,oBAAoB,OAAO,GAAG;AACvD,WAAO,EAAE,SAAS,MAAM,UAAU,WAAW;AAAA,EAC/C;AAEA,MAAI,QAAQ,SAAS,YAAY;AAC/B,UAAM,eAAe,sBAAsB;AAC3C,WAAO,EAAE,SAAS,MAAM,UAAU,aAAa;AAAA,EACjD;AAGA,MAAI,QAAQ,eAAe,CAAC,OAAO,KAAK;AACtC,WAAO;AAAA,MACL,SAAS;AAAA,MACT,UAAU,GAAG,QAAQ,IAAI,iCAAiC,QAAQ,IAAI;AAAA,MACtE,OAAO;AAAA,IACT;AAAA,EACF;AAGA,MAAI,QAAQ,cAAc,OAAO,KAAK;AACpC,QAAI,CAAC,cAAc,QAAQ,YAAY,OAAO,GAAG,GAAG;AAClD,aAAO;AAAA,QACL,SAAS;AAAA,QACT,UAAU,+BAA+B,QAAQ,IAAI;AAAA,QACrD,OAAO;AAAA,MACT;AAAA,IACF;AAAA,EACF;AAGA,MAAI,SAAS,QAAQ;AAErB,MAAI,UAAU,OAAO,KAAK;AAExB,QAAI,QAAQ,SAAS,WAAW;AAC9B,eAAS,gBAAgB,OAAO,GAAG;AAAA,IACrC,WAAW,QAAQ,SAAS,SAAS;AACnC,eAAS,eAAe,OAAO,GAAG;AAAA,IACpC;AAAA,EACF;AAGA,MAAI,QAAQ;AACV,YAAQ,IAAI,kCAAkC,MAAM,EAAE;AAEtD,UAAM,SAAS,MAAM,kBAAkB,QAAQ,OAAO;AAEtD,QAAI,OAAO,SAAS;AAClB,YAAM,SAAS,OAAO,QAAQ,MAAM,GAAG,GAAG,KAAK;AAC/C,aAAO;AAAA,QACL,SAAS;AAAA,QACT,UAAU,GAAG,QAAQ,IAAI,KAAK,MAAM;AAAA,QACpC;AAAA,MACF;AAAA,IACF,OAAO;AACL,aAAO;AAAA,QACL,SAAS;AAAA,QACT,UAAU,GAAG,QAAQ,IAAI,YAAY,OAAO,OAAO,MAAM,GAAG,GAAG,CAAC;AAAA,QAChE,OAAO,OAAO;AAAA,QACd;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAEA,SAAO;AAAA,IACL,SAAS;AAAA,IACT,UAAU,WAAW,QAAQ,IAAI;AAAA,EACnC;AACF;AAKA,eAAsB,oBACpB,UAC+C;AAC/C,QAAM,SAAS,MAAM,iBAAiB;AAAA,IACpC,MAAM;AAAA,IACN,OAAO;AAAA,IACP,SAAS;AAAA,EACX,CAAC;AAED,SAAO,EAAE,SAAS,OAAO,SAAS,OAAO,OAAO,MAAM;AACxD;AAKO,SAAS,iBAAuB;AACrC,QAAM,SAAS,mBAAmB;AAClC,SAAO,UAAU;AACjB,qBAAmB,MAAM;AAC3B;AAKO,SAAS,kBAAwB;AACtC,QAAM,SAAS,mBAAmB;AAClC,SAAO,UAAU;AACjB,qBAAmB,MAAM;AAC3B;AAKO,SAAS,oBAA6B;AAC3C,QAAM,SAAS,mBAAmB;AAClC,SAAO,OAAO;AAChB;AAKO,SAAS,WAAW,SAAgC;AACzD,QAAM,SAAS,mBAAmB;AAGlC,QAAM,gBAAgB,OAAO,SAAS;AAAA,IACpC,CAAC,MAAM,EAAE,KAAK,YAAY,MAAM,QAAQ,KAAK,YAAY;AAAA,EAC3D;AAEA,MAAI,iBAAiB,GAAG;AACtB,WAAO,SAAS,aAAa,IAAI;AAAA,EACnC,OAAO;AACL,WAAO,SAAS,KAAK,OAAO;AAAA,EAC9B;AAEA,qBAAmB,MAAM;AAC3B;AAKO,SAAS,cAAc,MAAuB;AACnD,QAAM,SAAS,mBAAmB;AAClC,QAAM,gBAAgB,OAAO,SAAS;AAEtC,SAAO,WAAW,OAAO,SAAS;AAAA,IAChC,CAAC,MAAM,EAAE,KAAK,YAAY,MAAM,KAAK,YAAY;AAAA,EACnD;AAEA,MAAI,OAAO,SAAS,SAAS,eAAe;AAC1C,uBAAmB,MAAM;AACzB,WAAO;AAAA,EACT;AAEA,SAAO;AACT;AAKO,SAAS,uBAA0C;AACxD,QAAM,SAAS,mBAAmB;AAClC,SAAO,OAAO,SAAS,OAAO,CAAC,MAAM,EAAE,OAAO;AAChD;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -12,6 +12,9 @@ import {
|
|
|
12
12
|
} from "./sms-notify.js";
|
|
13
13
|
import { writeFileSecure, ensureSecureDir } from "./secure-fs.js";
|
|
14
14
|
import { SyncOptionsSchema, parseConfigSafe } from "./schemas.js";
|
|
15
|
+
import {
|
|
16
|
+
frameLifecycleHooks
|
|
17
|
+
} from "../core/context/frame-lifecycle-hooks.js";
|
|
15
18
|
const SYNC_CONFIG_PATH = join(homedir(), ".stackmemory", "whatsapp-sync.json");
|
|
16
19
|
const DEFAULT_SYNC_OPTIONS = {
|
|
17
20
|
autoSyncOnClose: false,
|
|
@@ -285,6 +288,35 @@ async function onFrameClosed(frameData) {
|
|
|
285
288
|
const options = loadSyncOptions();
|
|
286
289
|
return syncFrameData(frameData, options);
|
|
287
290
|
}
|
|
291
|
+
async function handleFrameCloseHook(data) {
|
|
292
|
+
const digestData = createFrameDigestData(
|
|
293
|
+
data.frame,
|
|
294
|
+
data.events,
|
|
295
|
+
data.anchors
|
|
296
|
+
);
|
|
297
|
+
await onFrameClosed(digestData);
|
|
298
|
+
}
|
|
299
|
+
let hookUnregister = null;
|
|
300
|
+
function registerWhatsAppSyncHook() {
|
|
301
|
+
if (hookUnregister) {
|
|
302
|
+
return hookUnregister;
|
|
303
|
+
}
|
|
304
|
+
hookUnregister = frameLifecycleHooks.onFrameClosed(
|
|
305
|
+
"whatsapp-sync",
|
|
306
|
+
handleFrameCloseHook,
|
|
307
|
+
-10
|
|
308
|
+
// Low priority - run after other hooks
|
|
309
|
+
);
|
|
310
|
+
return () => {
|
|
311
|
+
if (hookUnregister) {
|
|
312
|
+
hookUnregister();
|
|
313
|
+
hookUnregister = null;
|
|
314
|
+
}
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
function isHookRegistered() {
|
|
318
|
+
return hookUnregister !== null;
|
|
319
|
+
}
|
|
288
320
|
function createFrameDigestData(frame, events, anchors) {
|
|
289
321
|
const now = Math.floor(Date.now() / 1e3);
|
|
290
322
|
const duration = (frame.closed_at || now) - frame.created_at;
|
|
@@ -365,8 +397,10 @@ export {
|
|
|
365
397
|
generateMobileDigest,
|
|
366
398
|
getFrameDigestData,
|
|
367
399
|
isAutoSyncEnabled,
|
|
400
|
+
isHookRegistered,
|
|
368
401
|
loadSyncOptions,
|
|
369
402
|
onFrameClosed,
|
|
403
|
+
registerWhatsAppSyncHook,
|
|
370
404
|
saveSyncOptions,
|
|
371
405
|
storeFrameDigest,
|
|
372
406
|
syncContext,
|