panopticon-cli 0.4.7 → 0.4.9
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-54LDKMHR.js → agents-2Q6TCHNN.js} +4 -3
- package/dist/{chunk-VTDDVLCK.js → chunk-45PZFXJM.js} +21 -19
- package/dist/{chunk-VTDDVLCK.js.map → chunk-45PZFXJM.js.map} +1 -1
- package/dist/{chunk-PUR532O7.js → chunk-65DGYSS4.js} +5 -207
- package/dist/chunk-65DGYSS4.js.map +1 -0
- package/dist/chunk-BBCUK6N2.js +241 -0
- package/dist/chunk-BBCUK6N2.js.map +1 -0
- package/dist/{chunk-ZZ3477GY.js → chunk-NRPHRN6M.js} +26 -9
- package/dist/chunk-NRPHRN6M.js.map +1 -0
- package/dist/{chunk-Z24TY3XN.js → chunk-XTXWSLT3.js} +2 -2
- package/dist/{chunk-F7NQZD6H.js → chunk-YOQLBJ7B.js} +2 -2
- package/dist/{chunk-NYVQC3D7.js → chunk-ZAM2Q52Q.js} +9 -2
- package/dist/{chunk-NYVQC3D7.js.map → chunk-ZAM2Q52Q.js.map} +1 -1
- package/dist/cli/index.js +59 -45
- package/dist/cli/index.js.map +1 -1
- package/dist/config-WCEPLNXI.js +15 -0
- package/dist/dashboard/public/assets/{index-DPSUbu4A.js → index-BnjdXycj.js} +109 -109
- package/dist/dashboard/public/assets/{index-CRqsEkmn.css → index-iqMjcW09.css} +1 -1
- package/dist/dashboard/public/index.html +2 -2
- package/dist/dashboard/server.js +6529 -6407
- package/dist/index.d.ts +6 -1
- package/dist/index.js +5 -2
- package/dist/index.js.map +1 -1
- package/dist/{remote-workspace-HI4VML6H.js → remote-workspace-WZVEDYQA.js} +4 -4
- package/dist/{specialist-context-SNCJ7O7G.js → specialist-context-JO4RB7UC.js} +4 -3
- package/dist/{specialist-context-SNCJ7O7G.js.map → specialist-context-JO4RB7UC.js.map} +1 -1
- package/dist/{specialist-logs-A7ODEK2T.js → specialist-logs-7P5EUIUT.js} +4 -3
- package/dist/{specialists-C7XLNSXQ.js → specialists-B4QAT244.js} +4 -3
- package/dist/{traefik-WI3KSRGG.js → traefik-DRNZMPGZ.js} +3 -3
- package/dist/traefik-DRNZMPGZ.js.map +1 -0
- package/package.json +2 -2
- package/scripts/heartbeat-hook +149 -0
- package/scripts/install-git-hooks.sh +66 -0
- package/scripts/notify-complete +79 -0
- package/scripts/postinstall.mjs +49 -0
- package/scripts/pre-tool-hook +60 -0
- package/scripts/record-cost-event.js +94 -0
- package/scripts/record-cost-event.ts +113 -0
- package/scripts/restart-dashboard.sh +59 -0
- package/scripts/setup-certs.sh +66 -0
- package/scripts/specialist-stop-hook +106 -0
- package/scripts/stop-hook +58 -0
- package/scripts/validate-merge.sh +119 -0
- package/dist/chunk-PUR532O7.js.map +0 -1
- package/dist/chunk-ZZ3477GY.js.map +0 -1
- /package/dist/{agents-54LDKMHR.js.map → agents-2Q6TCHNN.js.map} +0 -0
- /package/dist/{chunk-Z24TY3XN.js.map → chunk-XTXWSLT3.js.map} +0 -0
- /package/dist/{chunk-F7NQZD6H.js.map → chunk-YOQLBJ7B.js.map} +0 -0
- /package/dist/{specialist-logs-A7ODEK2T.js.map → config-WCEPLNXI.js.map} +0 -0
- /package/dist/{remote-workspace-HI4VML6H.js.map → remote-workspace-WZVEDYQA.js.map} +0 -0
- /package/dist/{specialists-C7XLNSXQ.js.map → specialist-logs-7P5EUIUT.js.map} +0 -0
- /package/dist/{traefik-WI3KSRGG.js.map → specialists-B4QAT244.js.map} +0 -0
package/dist/index.d.ts
CHANGED
|
@@ -295,6 +295,11 @@ interface PanopticonConfig {
|
|
|
295
295
|
declare function loadConfig(): PanopticonConfig;
|
|
296
296
|
declare function saveConfig(config: PanopticonConfig): void;
|
|
297
297
|
declare function getDefaultConfig(): PanopticonConfig;
|
|
298
|
+
/**
|
|
299
|
+
* Get the dashboard API base URL from config.
|
|
300
|
+
* Reads from DASHBOARD_URL env var first, then config file, then defaults.
|
|
301
|
+
*/
|
|
302
|
+
declare function getDashboardApiUrl(): string;
|
|
298
303
|
|
|
299
304
|
type Shell = 'bash' | 'zsh' | 'fish' | 'unknown';
|
|
300
305
|
declare function detectShell(): Shell;
|
|
@@ -702,4 +707,4 @@ declare function needsRouter(apiKeys: {
|
|
|
702
707
|
*/
|
|
703
708
|
declare function getProviderEnv(provider: ProviderConfig, apiKey: string): Record<string, string>;
|
|
704
709
|
|
|
705
|
-
export { AGENTS_DIR, type AnthropicModel, type ApiKeysConfig, BACKUPS_DIR, BIN_DIR, type BackupInfo, CERTS_DIR, CLAUDE_DIR, CLAUDE_MD_TEMPLATES, CODEX_DIR, COMMANDS_DIR, CONFIG_DIR, CONFIG_FILE, COSTS_DIR, CURSOR_DIR, type Comment, type ComplexityLevel, type ComplexityModels, GEMINI_DIR, type GitHubConfig, GitHubTracker, type GitLabConfig, GitLabTracker, type GoogleModel, HEARTBEATS_DIR, type HookItem, INIT_DIRS, type Issue, type IssueFilters, IssueNotFoundError, type IssueState, type IssueTracker, type IssueUpdate, type KimiModel, type LinearConfig, LinearTracker, type LinkDirection, LinkManager, type ModelId, type ModelsConfig, type NewIssue, NotImplementedError, OPENCODE_DIR, type OpenAIModel, PANOPTICON_HOME, PROVIDERS, type PanopticonConfig, type ProviderCompatibility, type ProviderConfig, type ProviderName, type RallyConfig, type RemoteConfig, type RemoteExeConfig, type Runtime, SETTINGS_FILE, SKILLS_DIR, SOURCE_DEV_SKILLS_DIR, SOURCE_SCRIPTS_DIR, SOURCE_SKILLS_DIR, SOURCE_TEMPLATES_DIR, SOURCE_TRAEFIK_TEMPLATES, SYNC_TARGETS, type SettingsConfig, type ShadowConfig, type Shell, type SpecialistModels, type SyncItem, type SyncOptions, type SyncPlan, type SyncResult, TEMPLATES_DIR, TRAEFIK_CERTS_DIR, TRAEFIK_DIR, TRAEFIK_DYNAMIC_DIR, TrackerAuthError, type TrackerConfig, type TrackerConfigItem, type TrackerLink, type TrackerType, type TrackersConfig, type ZAIModel, addAlias, cleanOldBackups, createBackup, createBackupTimestamp, createTracker, createTrackerFromConfig, detectShell, executeSync, formatIssueRef, getAgentCommand, getAliasInstructions, getAllTrackers, getAvailableModels, getClaudeModelFlag, getDefaultConfig, getDefaultSettings, getDirectProviders, getLinkManager, getPrimaryTracker, getProviderEnv, getProviderForModel, getRouterProviders, getSecondaryTracker, getShellRcFile, hasAlias, isAnthropicModel, isDevMode, isPanopticonSymlink, listBackups, loadConfig, loadSettings, needsRouter, parseIssueRef, planHooksSync, planSync, requiresRouter, restoreBackup, saveConfig, saveSettings, syncHooks, validateSettings };
|
|
710
|
+
export { AGENTS_DIR, type AnthropicModel, type ApiKeysConfig, BACKUPS_DIR, BIN_DIR, type BackupInfo, CERTS_DIR, CLAUDE_DIR, CLAUDE_MD_TEMPLATES, CODEX_DIR, COMMANDS_DIR, CONFIG_DIR, CONFIG_FILE, COSTS_DIR, CURSOR_DIR, type Comment, type ComplexityLevel, type ComplexityModels, GEMINI_DIR, type GitHubConfig, GitHubTracker, type GitLabConfig, GitLabTracker, type GoogleModel, HEARTBEATS_DIR, type HookItem, INIT_DIRS, type Issue, type IssueFilters, IssueNotFoundError, type IssueState, type IssueTracker, type IssueUpdate, type KimiModel, type LinearConfig, LinearTracker, type LinkDirection, LinkManager, type ModelId, type ModelsConfig, type NewIssue, NotImplementedError, OPENCODE_DIR, type OpenAIModel, PANOPTICON_HOME, PROVIDERS, type PanopticonConfig, type ProviderCompatibility, type ProviderConfig, type ProviderName, type RallyConfig, type RemoteConfig, type RemoteExeConfig, type Runtime, SETTINGS_FILE, SKILLS_DIR, SOURCE_DEV_SKILLS_DIR, SOURCE_SCRIPTS_DIR, SOURCE_SKILLS_DIR, SOURCE_TEMPLATES_DIR, SOURCE_TRAEFIK_TEMPLATES, SYNC_TARGETS, type SettingsConfig, type ShadowConfig, type Shell, type SpecialistModels, type SyncItem, type SyncOptions, type SyncPlan, type SyncResult, TEMPLATES_DIR, TRAEFIK_CERTS_DIR, TRAEFIK_DIR, TRAEFIK_DYNAMIC_DIR, TrackerAuthError, type TrackerConfig, type TrackerConfigItem, type TrackerLink, type TrackerType, type TrackersConfig, type ZAIModel, addAlias, cleanOldBackups, createBackup, createBackupTimestamp, createTracker, createTrackerFromConfig, detectShell, executeSync, formatIssueRef, getAgentCommand, getAliasInstructions, getAllTrackers, getAvailableModels, getClaudeModelFlag, getDashboardApiUrl, getDefaultConfig, getDefaultSettings, getDirectProviders, getLinkManager, getPrimaryTracker, getProviderEnv, getProviderForModel, getRouterProviders, getSecondaryTracker, getShellRcFile, hasAlias, isAnthropicModel, isDevMode, isPanopticonSymlink, listBackups, loadConfig, loadSettings, needsRouter, parseIssueRef, planHooksSync, planSync, requiresRouter, restoreBackup, saveConfig, saveSettings, syncHooks, validateSettings };
|
package/dist/index.js
CHANGED
|
@@ -29,7 +29,7 @@ import {
|
|
|
29
29
|
planSync,
|
|
30
30
|
restoreBackup,
|
|
31
31
|
syncHooks
|
|
32
|
-
} from "./chunk-
|
|
32
|
+
} from "./chunk-NRPHRN6M.js";
|
|
33
33
|
import {
|
|
34
34
|
PROVIDERS,
|
|
35
35
|
getAgentCommand,
|
|
@@ -49,11 +49,13 @@ import {
|
|
|
49
49
|
saveSettings,
|
|
50
50
|
validateSettings
|
|
51
51
|
} from "./chunk-KQAEUOML.js";
|
|
52
|
+
import "./chunk-BBCUK6N2.js";
|
|
52
53
|
import {
|
|
54
|
+
getDashboardApiUrl,
|
|
53
55
|
getDefaultConfig,
|
|
54
56
|
loadConfig,
|
|
55
57
|
saveConfig
|
|
56
|
-
} from "./chunk-
|
|
58
|
+
} from "./chunk-ZAM2Q52Q.js";
|
|
57
59
|
import {
|
|
58
60
|
AGENTS_DIR,
|
|
59
61
|
BACKUPS_DIR,
|
|
@@ -148,6 +150,7 @@ export {
|
|
|
148
150
|
getAllTrackers,
|
|
149
151
|
getAvailableModels,
|
|
150
152
|
getClaudeModelFlag,
|
|
153
|
+
getDashboardApiUrl,
|
|
151
154
|
getDefaultConfig,
|
|
152
155
|
getDefaultSettings,
|
|
153
156
|
getDirectProviders,
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["// Panopticon CLI - Main exports for library usage\nexport * from './lib/paths.js';\nexport * from './lib/config.js';\nexport * from './lib/shell.js';\nexport * from './lib/backup.js';\nexport * from './lib/sync.js';\nexport * from './lib/tracker/index.js';\nexport * from './lib/providers.js';\nexport * from './lib/settings.js';\n"],"mappings":"
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["// Panopticon CLI - Main exports for library usage\nexport * from './lib/paths.js';\nexport * from './lib/config.js';\nexport * from './lib/shell.js';\nexport * from './lib/backup.js';\nexport * from './lib/sync.js';\nexport * from './lib/tracker/index.js';\nexport * from './lib/providers.js';\nexport * from './lib/settings.js';\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AACA;AAMA;AACA;","names":[]}
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import {
|
|
2
2
|
saveWorkspaceMetadata
|
|
3
3
|
} from "./chunk-44EOY2ZL.js";
|
|
4
|
+
import {
|
|
5
|
+
loadConfig
|
|
6
|
+
} from "./chunk-ZAM2Q52Q.js";
|
|
4
7
|
import {
|
|
5
8
|
extractTeamPrefix,
|
|
6
9
|
findProjectByTeam,
|
|
@@ -10,9 +13,6 @@ import {
|
|
|
10
13
|
createExeProvider,
|
|
11
14
|
init_exe_provider
|
|
12
15
|
} from "./chunk-JM6V62LT.js";
|
|
13
|
-
import {
|
|
14
|
-
loadConfig
|
|
15
|
-
} from "./chunk-NYVQC3D7.js";
|
|
16
16
|
import "./chunk-KGPRXDMX.js";
|
|
17
17
|
import {
|
|
18
18
|
init_esm_shims
|
|
@@ -176,4 +176,4 @@ EOF`);
|
|
|
176
176
|
export {
|
|
177
177
|
createRemoteWorkspace
|
|
178
178
|
};
|
|
179
|
-
//# sourceMappingURL=remote-workspace-
|
|
179
|
+
//# sourceMappingURL=remote-workspace-WZVEDYQA.js.map
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import {
|
|
2
2
|
getRecentRunLogs,
|
|
3
3
|
init_specialist_logs
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-45PZFXJM.js";
|
|
5
5
|
import {
|
|
6
6
|
getModelId,
|
|
7
7
|
init_work_type_router
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-65DGYSS4.js";
|
|
9
|
+
import "./chunk-BBCUK6N2.js";
|
|
9
10
|
import {
|
|
10
11
|
getProject,
|
|
11
12
|
init_projects
|
|
@@ -253,4 +254,4 @@ export {
|
|
|
253
254
|
regenerateContextDigest,
|
|
254
255
|
scheduleDigestGeneration
|
|
255
256
|
};
|
|
256
|
-
//# sourceMappingURL=specialist-context-
|
|
257
|
+
//# sourceMappingURL=specialist-context-JO4RB7UC.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/lib/cloister/specialist-context.ts"],"sourcesContent":["/**\n * Specialist Context Management\n *\n * Generates and manages AI-powered context digests from recent specialist runs.\n * These digests seed new specialist sessions with learned patterns and expertise.\n *\n * Directory structure:\n * ~/.panopticon/specialists/{projectKey}/{specialistType}/context/latest-digest.md\n */\n\nimport { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';\nimport { join } from 'path';\nimport { exec } from 'child_process';\nimport { promisify } from 'util';\nimport { PANOPTICON_HOME } from '../paths.js';\nimport { getRecentRunLogs, type RunLogEntry } from './specialist-logs.js';\nimport { getProject } from '../projects.js';\nimport { getModelId } from '../work-type-router.js';\n\nconst execAsync = promisify(exec);\n\nconst SPECIALISTS_DIR = join(PANOPTICON_HOME, 'specialists');\n\n/**\n * Get the context directory for a project's specialist\n */\nexport function getContextDirectory(projectKey: string, specialistType: string): string {\n return join(SPECIALISTS_DIR, projectKey, specialistType, 'context');\n}\n\n/**\n * Get the path to the latest context digest file\n */\nexport function getContextDigestPath(projectKey: string, specialistType: string): string {\n const contextDir = getContextDirectory(projectKey, specialistType);\n return join(contextDir, 'latest-digest.md');\n}\n\n/**\n * Ensure context directory exists for a project's specialist\n */\nfunction ensureContextDirectory(projectKey: string, specialistType: string): void {\n const contextDir = getContextDirectory(projectKey, specialistType);\n if (!existsSync(contextDir)) {\n mkdirSync(contextDir, { recursive: true });\n }\n}\n\n/**\n * Load the context digest for a specialist\n *\n * @param projectKey - Project identifier\n * @param specialistType - Specialist type\n * @returns Context digest content or null if not found\n */\nexport function loadContextDigest(projectKey: string, specialistType: string): string | null {\n const digestPath = getContextDigestPath(projectKey, specialistType);\n\n if (!existsSync(digestPath)) {\n return null;\n }\n\n try {\n return readFileSync(digestPath, 'utf-8');\n } catch (error) {\n console.error(`[specialist-context] Failed to load digest for ${projectKey}/${specialistType}:`, error);\n return null;\n }\n}\n\n/**\n * Get the number of recent runs to include in context\n *\n * Reads from project config or uses default.\n *\n * @param projectKey - Project identifier\n * @returns Number of runs to include (default: 5)\n */\nfunction getContextRunsCount(projectKey: string): number {\n const project = getProject(projectKey);\n return project?.specialists?.context_runs ?? 5;\n}\n\n/**\n * Get the model to use for digest generation\n *\n * Reads from project config or uses the same model as the specialist.\n *\n * @param projectKey - Project identifier\n * @param specialistType - Specialist type\n * @returns Model ID to use\n */\nfunction getDigestModel(projectKey: string, specialistType: string): string {\n const project = getProject(projectKey);\n\n // Check for explicit digest model in project config\n if (project?.specialists?.digest_model) {\n return project.specialists.digest_model;\n }\n\n // Fall back to specialist's model\n try {\n const workTypeId = `specialist-${specialistType}` as any;\n return getModelId(workTypeId);\n } catch (error) {\n // Default to Sonnet if can't resolve\n return 'claude-sonnet-4-5';\n }\n}\n\n/**\n * Generate a context digest from recent runs using AI\n *\n * Creates an AI-generated summary of recent specialist runs to provide\n * context for the next run. This includes patterns, learnings, and common issues.\n *\n * @param projectKey - Project identifier\n * @param specialistType - Specialist type\n * @param options - Generation options\n * @returns Generated digest or null if generation failed\n */\nexport async function generateContextDigest(\n projectKey: string,\n specialistType: string,\n options: {\n runCount?: number;\n model?: string;\n force?: boolean; // Generate even if no recent runs\n } = {}\n): Promise<string | null> {\n ensureContextDirectory(projectKey, specialistType);\n\n // Get recent runs\n const runCount = options.runCount ?? getContextRunsCount(projectKey);\n const recentRuns = getRecentRunLogs(projectKey, specialistType, runCount);\n\n if (recentRuns.length === 0 && !options.force) {\n console.log(`[specialist-context] No recent runs for ${projectKey}/${specialistType}, skipping digest generation`);\n return null;\n }\n\n // Build prompt for digest generation\n const prompt = buildDigestPrompt(projectKey, specialistType, recentRuns);\n const model = options.model ?? getDigestModel(projectKey, specialistType);\n\n try {\n console.log(`[specialist-context] Generating digest for ${projectKey}/${specialistType} using ${model}...`);\n\n // Use Claude Code CLI to generate digest\n // Write prompt to temp file to avoid shell escaping issues\n const tempDir = join(PANOPTICON_HOME, 'tmp');\n if (!existsSync(tempDir)) {\n mkdirSync(tempDir, { recursive: true });\n }\n\n const promptFile = join(tempDir, `digest-prompt-${Date.now()}.md`);\n writeFileSync(promptFile, prompt, 'utf-8');\n\n // Run Claude Code with the prompt\n const { stdout, stderr } = await execAsync(\n `claude --dangerously-skip-permissions --model ${model} \"$(cat '${promptFile}')\"`,\n {\n encoding: 'utf-8',\n maxBuffer: 10 * 1024 * 1024, // 10MB buffer\n timeout: 60000, // 60 second timeout\n }\n );\n\n // Clean up temp file\n try {\n const { unlinkSync } = await import('fs');\n unlinkSync(promptFile);\n } catch {\n // Ignore cleanup errors\n }\n\n if (stderr && !stderr.includes('warning')) {\n console.error(`[specialist-context] Claude stderr:`, stderr);\n }\n\n const digest = stdout.trim();\n\n if (!digest) {\n console.error(`[specialist-context] Empty digest generated`);\n return null;\n }\n\n // Save digest\n const digestPath = getContextDigestPath(projectKey, specialistType);\n writeFileSync(digestPath, digest, 'utf-8');\n\n console.log(`[specialist-context] Generated digest (${digest.length} chars)`);\n return digest;\n } catch (error: any) {\n console.error(`[specialist-context] Failed to generate digest:`, error.message);\n // Degrade gracefully - return null so specialist can continue without context\n return null;\n }\n}\n\n/**\n * Build the prompt for digest generation\n *\n * @param projectKey - Project identifier\n * @param specialistType - Specialist type\n * @param recentRuns - Recent run logs\n * @returns Prompt for Claude\n */\nfunction buildDigestPrompt(\n projectKey: string,\n specialistType: string,\n recentRuns: RunLogEntry[]\n): string {\n const project = getProject(projectKey);\n const projectName = project?.name || projectKey;\n\n let prompt = `You are analyzing the recent history of a ${specialistType} specialist for the ${projectName} project.\n\nYour task is to generate a concise context digest that will be provided to the specialist at the start of their next run. This digest should help them understand:\n- Common patterns and practices observed in recent runs\n- Recurring issues or failure modes\n- Successful approaches and best practices\n- Any project-specific context that would be helpful\n\nGenerate a digest in markdown format. Keep it focused and actionable - aim for 200-400 words total.\n\n## Recent Runs\n\n`;\n\n if (recentRuns.length === 0) {\n prompt += `No recent runs available yet. This is the specialist's first run.\\n\\n`;\n prompt += `Generate a brief introduction for the specialist explaining their role and what to expect.\\n`;\n } else {\n recentRuns.forEach((run, index) => {\n prompt += `### Run ${index + 1}: ${run.metadata.issueId} (${run.metadata.status || 'unknown'})\\n`;\n prompt += `Started: ${run.metadata.startedAt}\\n`;\n if (run.metadata.finishedAt) {\n prompt += `Finished: ${run.metadata.finishedAt}\\n`;\n }\n if (run.metadata.duration) {\n const durationSec = Math.floor(run.metadata.duration / 1000);\n const minutes = Math.floor(durationSec / 60);\n const seconds = durationSec % 60;\n prompt += `Duration: ${minutes}m ${seconds}s\\n`;\n }\n if (run.metadata.notes) {\n prompt += `Notes: ${run.metadata.notes}\\n`;\n }\n\n // Include snippets from the log if available\n try {\n const logContent = readFileSync(run.filePath, 'utf-8');\n // Extract key sections (limit to avoid overwhelming the prompt)\n const maxChars = 500;\n const transcriptMatch = logContent.match(/## Session Transcript\\n([\\s\\S]+?)(?=\\n## |$)/);\n if (transcriptMatch) {\n let transcript = transcriptMatch[1].trim();\n if (transcript.length > maxChars) {\n transcript = transcript.substring(0, maxChars) + '... [truncated]';\n }\n prompt += `\\nTranscript excerpt:\\n${transcript}\\n`;\n }\n } catch (error) {\n // If we can't read the log, skip the excerpt\n }\n\n prompt += `\\n`;\n });\n }\n\n prompt += `\\n## Your Task\n\nGenerate a context digest that summarizes the key insights from these runs. Format it as:\n\n# Recent ${specialistType} History for ${projectName}\n\n## Summary\n[2-3 sentence overview of patterns and trends]\n\n## Common Patterns\n[Bulleted list of observed patterns]\n\n## Recent Notable Runs\n[Brief highlights of 2-3 most interesting runs]\n\n## Recommendations\n[Specific guidance for the next run based on this history]\n\nKeep it concise, actionable, and focused on helping the specialist be more effective.`;\n\n return prompt;\n}\n\n/**\n * Regenerate the context digest\n *\n * Forces regeneration even if a digest already exists.\n *\n * @param projectKey - Project identifier\n * @param specialistType - Specialist type\n * @returns Generated digest or null if generation failed\n */\nexport async function regenerateContextDigest(\n projectKey: string,\n specialistType: string\n): Promise<string | null> {\n return generateContextDigest(projectKey, specialistType, { force: true });\n}\n\n/**\n * Generate digest after a run completes (async, fire-and-forget)\n *\n * This is called after a specialist finishes a run to update the context\n * for the next run. It runs asynchronously and failures are logged but not thrown.\n *\n * @param projectKey - Project identifier\n * @param specialistType - Specialist type\n */\nexport function scheduleDigestGeneration(projectKey: string, specialistType: string): void {\n // Run async without awaiting\n generateContextDigest(projectKey, specialistType).catch((error) => {\n console.error(\n `[specialist-context] Background digest generation failed for ${projectKey}/${specialistType}:`,\n error\n );\n });\n}\n\n/**\n * Check if a context digest exists\n *\n * @param projectKey - Project identifier\n * @param specialistType - Specialist type\n * @returns True if digest file exists\n */\nexport function hasContextDigest(projectKey: string, specialistType: string): boolean {\n const digestPath = getContextDigestPath(projectKey, specialistType);\n return existsSync(digestPath);\n}\n\n/**\n * Delete the context digest\n *\n * Useful for forcing a fresh start or clearing stale context.\n *\n * @param projectKey - Project identifier\n * @param specialistType - Specialist type\n * @returns True if digest was deleted, false if it didn't exist\n */\nexport function deleteContextDigest(projectKey: string, specialistType: string): boolean {\n const digestPath = getContextDigestPath(projectKey, specialistType);\n\n if (!existsSync(digestPath)) {\n return false;\n }\n\n try {\n const { unlinkSync } = require('fs');\n unlinkSync(digestPath);\n return true;\n } catch (error) {\n console.error(`[specialist-context] Failed to delete digest:`, error);\n return false;\n }\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/lib/cloister/specialist-context.ts"],"sourcesContent":["/**\n * Specialist Context Management\n *\n * Generates and manages AI-powered context digests from recent specialist runs.\n * These digests seed new specialist sessions with learned patterns and expertise.\n *\n * Directory structure:\n * ~/.panopticon/specialists/{projectKey}/{specialistType}/context/latest-digest.md\n */\n\nimport { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';\nimport { join } from 'path';\nimport { exec } from 'child_process';\nimport { promisify } from 'util';\nimport { PANOPTICON_HOME } from '../paths.js';\nimport { getRecentRunLogs, type RunLogEntry } from './specialist-logs.js';\nimport { getProject } from '../projects.js';\nimport { getModelId } from '../work-type-router.js';\n\nconst execAsync = promisify(exec);\n\nconst SPECIALISTS_DIR = join(PANOPTICON_HOME, 'specialists');\n\n/**\n * Get the context directory for a project's specialist\n */\nexport function getContextDirectory(projectKey: string, specialistType: string): string {\n return join(SPECIALISTS_DIR, projectKey, specialistType, 'context');\n}\n\n/**\n * Get the path to the latest context digest file\n */\nexport function getContextDigestPath(projectKey: string, specialistType: string): string {\n const contextDir = getContextDirectory(projectKey, specialistType);\n return join(contextDir, 'latest-digest.md');\n}\n\n/**\n * Ensure context directory exists for a project's specialist\n */\nfunction ensureContextDirectory(projectKey: string, specialistType: string): void {\n const contextDir = getContextDirectory(projectKey, specialistType);\n if (!existsSync(contextDir)) {\n mkdirSync(contextDir, { recursive: true });\n }\n}\n\n/**\n * Load the context digest for a specialist\n *\n * @param projectKey - Project identifier\n * @param specialistType - Specialist type\n * @returns Context digest content or null if not found\n */\nexport function loadContextDigest(projectKey: string, specialistType: string): string | null {\n const digestPath = getContextDigestPath(projectKey, specialistType);\n\n if (!existsSync(digestPath)) {\n return null;\n }\n\n try {\n return readFileSync(digestPath, 'utf-8');\n } catch (error) {\n console.error(`[specialist-context] Failed to load digest for ${projectKey}/${specialistType}:`, error);\n return null;\n }\n}\n\n/**\n * Get the number of recent runs to include in context\n *\n * Reads from project config or uses default.\n *\n * @param projectKey - Project identifier\n * @returns Number of runs to include (default: 5)\n */\nfunction getContextRunsCount(projectKey: string): number {\n const project = getProject(projectKey);\n return project?.specialists?.context_runs ?? 5;\n}\n\n/**\n * Get the model to use for digest generation\n *\n * Reads from project config or uses the same model as the specialist.\n *\n * @param projectKey - Project identifier\n * @param specialistType - Specialist type\n * @returns Model ID to use\n */\nfunction getDigestModel(projectKey: string, specialistType: string): string {\n const project = getProject(projectKey);\n\n // Check for explicit digest model in project config\n if (project?.specialists?.digest_model) {\n return project.specialists.digest_model;\n }\n\n // Fall back to specialist's model\n try {\n const workTypeId = `specialist-${specialistType}` as any;\n return getModelId(workTypeId);\n } catch (error) {\n // Default to Sonnet if can't resolve\n return 'claude-sonnet-4-5';\n }\n}\n\n/**\n * Generate a context digest from recent runs using AI\n *\n * Creates an AI-generated summary of recent specialist runs to provide\n * context for the next run. This includes patterns, learnings, and common issues.\n *\n * @param projectKey - Project identifier\n * @param specialistType - Specialist type\n * @param options - Generation options\n * @returns Generated digest or null if generation failed\n */\nexport async function generateContextDigest(\n projectKey: string,\n specialistType: string,\n options: {\n runCount?: number;\n model?: string;\n force?: boolean; // Generate even if no recent runs\n } = {}\n): Promise<string | null> {\n ensureContextDirectory(projectKey, specialistType);\n\n // Get recent runs\n const runCount = options.runCount ?? getContextRunsCount(projectKey);\n const recentRuns = getRecentRunLogs(projectKey, specialistType, runCount);\n\n if (recentRuns.length === 0 && !options.force) {\n console.log(`[specialist-context] No recent runs for ${projectKey}/${specialistType}, skipping digest generation`);\n return null;\n }\n\n // Build prompt for digest generation\n const prompt = buildDigestPrompt(projectKey, specialistType, recentRuns);\n const model = options.model ?? getDigestModel(projectKey, specialistType);\n\n try {\n console.log(`[specialist-context] Generating digest for ${projectKey}/${specialistType} using ${model}...`);\n\n // Use Claude Code CLI to generate digest\n // Write prompt to temp file to avoid shell escaping issues\n const tempDir = join(PANOPTICON_HOME, 'tmp');\n if (!existsSync(tempDir)) {\n mkdirSync(tempDir, { recursive: true });\n }\n\n const promptFile = join(tempDir, `digest-prompt-${Date.now()}.md`);\n writeFileSync(promptFile, prompt, 'utf-8');\n\n // Run Claude Code with the prompt\n const { stdout, stderr } = await execAsync(\n `claude --dangerously-skip-permissions --model ${model} \"$(cat '${promptFile}')\"`,\n {\n encoding: 'utf-8',\n maxBuffer: 10 * 1024 * 1024, // 10MB buffer\n timeout: 60000, // 60 second timeout\n }\n );\n\n // Clean up temp file\n try {\n const { unlinkSync } = await import('fs');\n unlinkSync(promptFile);\n } catch {\n // Ignore cleanup errors\n }\n\n if (stderr && !stderr.includes('warning')) {\n console.error(`[specialist-context] Claude stderr:`, stderr);\n }\n\n const digest = stdout.trim();\n\n if (!digest) {\n console.error(`[specialist-context] Empty digest generated`);\n return null;\n }\n\n // Save digest\n const digestPath = getContextDigestPath(projectKey, specialistType);\n writeFileSync(digestPath, digest, 'utf-8');\n\n console.log(`[specialist-context] Generated digest (${digest.length} chars)`);\n return digest;\n } catch (error: any) {\n console.error(`[specialist-context] Failed to generate digest:`, error.message);\n // Degrade gracefully - return null so specialist can continue without context\n return null;\n }\n}\n\n/**\n * Build the prompt for digest generation\n *\n * @param projectKey - Project identifier\n * @param specialistType - Specialist type\n * @param recentRuns - Recent run logs\n * @returns Prompt for Claude\n */\nfunction buildDigestPrompt(\n projectKey: string,\n specialistType: string,\n recentRuns: RunLogEntry[]\n): string {\n const project = getProject(projectKey);\n const projectName = project?.name || projectKey;\n\n let prompt = `You are analyzing the recent history of a ${specialistType} specialist for the ${projectName} project.\n\nYour task is to generate a concise context digest that will be provided to the specialist at the start of their next run. This digest should help them understand:\n- Common patterns and practices observed in recent runs\n- Recurring issues or failure modes\n- Successful approaches and best practices\n- Any project-specific context that would be helpful\n\nGenerate a digest in markdown format. Keep it focused and actionable - aim for 200-400 words total.\n\n## Recent Runs\n\n`;\n\n if (recentRuns.length === 0) {\n prompt += `No recent runs available yet. This is the specialist's first run.\\n\\n`;\n prompt += `Generate a brief introduction for the specialist explaining their role and what to expect.\\n`;\n } else {\n recentRuns.forEach((run, index) => {\n prompt += `### Run ${index + 1}: ${run.metadata.issueId} (${run.metadata.status || 'unknown'})\\n`;\n prompt += `Started: ${run.metadata.startedAt}\\n`;\n if (run.metadata.finishedAt) {\n prompt += `Finished: ${run.metadata.finishedAt}\\n`;\n }\n if (run.metadata.duration) {\n const durationSec = Math.floor(run.metadata.duration / 1000);\n const minutes = Math.floor(durationSec / 60);\n const seconds = durationSec % 60;\n prompt += `Duration: ${minutes}m ${seconds}s\\n`;\n }\n if (run.metadata.notes) {\n prompt += `Notes: ${run.metadata.notes}\\n`;\n }\n\n // Include snippets from the log if available\n try {\n const logContent = readFileSync(run.filePath, 'utf-8');\n // Extract key sections (limit to avoid overwhelming the prompt)\n const maxChars = 500;\n const transcriptMatch = logContent.match(/## Session Transcript\\n([\\s\\S]+?)(?=\\n## |$)/);\n if (transcriptMatch) {\n let transcript = transcriptMatch[1].trim();\n if (transcript.length > maxChars) {\n transcript = transcript.substring(0, maxChars) + '... [truncated]';\n }\n prompt += `\\nTranscript excerpt:\\n${transcript}\\n`;\n }\n } catch (error) {\n // If we can't read the log, skip the excerpt\n }\n\n prompt += `\\n`;\n });\n }\n\n prompt += `\\n## Your Task\n\nGenerate a context digest that summarizes the key insights from these runs. Format it as:\n\n# Recent ${specialistType} History for ${projectName}\n\n## Summary\n[2-3 sentence overview of patterns and trends]\n\n## Common Patterns\n[Bulleted list of observed patterns]\n\n## Recent Notable Runs\n[Brief highlights of 2-3 most interesting runs]\n\n## Recommendations\n[Specific guidance for the next run based on this history]\n\nKeep it concise, actionable, and focused on helping the specialist be more effective.`;\n\n return prompt;\n}\n\n/**\n * Regenerate the context digest\n *\n * Forces regeneration even if a digest already exists.\n *\n * @param projectKey - Project identifier\n * @param specialistType - Specialist type\n * @returns Generated digest or null if generation failed\n */\nexport async function regenerateContextDigest(\n projectKey: string,\n specialistType: string\n): Promise<string | null> {\n return generateContextDigest(projectKey, specialistType, { force: true });\n}\n\n/**\n * Generate digest after a run completes (async, fire-and-forget)\n *\n * This is called after a specialist finishes a run to update the context\n * for the next run. It runs asynchronously and failures are logged but not thrown.\n *\n * @param projectKey - Project identifier\n * @param specialistType - Specialist type\n */\nexport function scheduleDigestGeneration(projectKey: string, specialistType: string): void {\n // Run async without awaiting\n generateContextDigest(projectKey, specialistType).catch((error) => {\n console.error(\n `[specialist-context] Background digest generation failed for ${projectKey}/${specialistType}:`,\n error\n );\n });\n}\n\n/**\n * Check if a context digest exists\n *\n * @param projectKey - Project identifier\n * @param specialistType - Specialist type\n * @returns True if digest file exists\n */\nexport function hasContextDigest(projectKey: string, specialistType: string): boolean {\n const digestPath = getContextDigestPath(projectKey, specialistType);\n return existsSync(digestPath);\n}\n\n/**\n * Delete the context digest\n *\n * Useful for forcing a fresh start or clearing stale context.\n *\n * @param projectKey - Project identifier\n * @param specialistType - Specialist type\n * @returns True if digest was deleted, false if it didn't exist\n */\nexport function deleteContextDigest(projectKey: string, specialistType: string): boolean {\n const digestPath = getContextDigestPath(projectKey, specialistType);\n\n if (!existsSync(digestPath)) {\n return false;\n }\n\n try {\n const { unlinkSync } = require('fs');\n unlinkSync(digestPath);\n return true;\n } catch (error) {\n console.error(`[specialist-context] Failed to delete digest:`, error);\n return false;\n }\n}\n"],"mappings":"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","names":[]}
|
|
@@ -16,8 +16,9 @@ import {
|
|
|
16
16
|
isRunLogActive,
|
|
17
17
|
listRunLogs,
|
|
18
18
|
parseLogMetadata
|
|
19
|
-
} from "./chunk-
|
|
20
|
-
import "./chunk-
|
|
19
|
+
} from "./chunk-45PZFXJM.js";
|
|
20
|
+
import "./chunk-65DGYSS4.js";
|
|
21
|
+
import "./chunk-BBCUK6N2.js";
|
|
21
22
|
import "./chunk-K45YD6A3.js";
|
|
22
23
|
import "./chunk-KGPRXDMX.js";
|
|
23
24
|
import "./chunk-ZHC57RCV.js";
|
|
@@ -40,4 +41,4 @@ export {
|
|
|
40
41
|
listRunLogs,
|
|
41
42
|
parseLogMetadata
|
|
42
43
|
};
|
|
43
|
-
//# sourceMappingURL=specialist-logs-
|
|
44
|
+
//# sourceMappingURL=specialist-logs-7P5EUIUT.js.map
|
|
@@ -55,8 +55,9 @@ import {
|
|
|
55
55
|
wakeSpecialist,
|
|
56
56
|
wakeSpecialistOrQueue,
|
|
57
57
|
wakeSpecialistWithTask
|
|
58
|
-
} from "./chunk-
|
|
59
|
-
import "./chunk-
|
|
58
|
+
} from "./chunk-45PZFXJM.js";
|
|
59
|
+
import "./chunk-65DGYSS4.js";
|
|
60
|
+
import "./chunk-BBCUK6N2.js";
|
|
60
61
|
import "./chunk-K45YD6A3.js";
|
|
61
62
|
import "./chunk-KGPRXDMX.js";
|
|
62
63
|
import "./chunk-ZHC57RCV.js";
|
|
@@ -118,4 +119,4 @@ export {
|
|
|
118
119
|
wakeSpecialistOrQueue,
|
|
119
120
|
wakeSpecialistWithTask
|
|
120
121
|
};
|
|
121
|
-
//# sourceMappingURL=specialists-
|
|
122
|
+
//# sourceMappingURL=specialists-B4QAT244.js.map
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import {
|
|
2
2
|
cleanupTemplateFiles,
|
|
3
3
|
generatePanopticonTraefikConfig
|
|
4
|
-
} from "./chunk-
|
|
5
|
-
import "./chunk-
|
|
4
|
+
} from "./chunk-YOQLBJ7B.js";
|
|
5
|
+
import "./chunk-ZAM2Q52Q.js";
|
|
6
6
|
import "./chunk-KGPRXDMX.js";
|
|
7
7
|
import "./chunk-ZHC57RCV.js";
|
|
8
8
|
export {
|
|
9
9
|
cleanupTemplateFiles,
|
|
10
10
|
generatePanopticonTraefikConfig
|
|
11
11
|
};
|
|
12
|
-
//# sourceMappingURL=traefik-
|
|
12
|
+
//# sourceMappingURL=traefik-DRNZMPGZ.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "panopticon-cli",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.9",
|
|
4
4
|
"description": "Multi-agent orchestration for AI coding assistants (Claude Code, Codex, Cursor, Gemini CLI)",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai-agents",
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"dist",
|
|
40
40
|
"templates",
|
|
41
41
|
"skills",
|
|
42
|
-
"scripts
|
|
42
|
+
"scripts",
|
|
43
43
|
"README.md",
|
|
44
44
|
"LICENSE"
|
|
45
45
|
],
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# ~/.panopticon/bin/heartbeat-hook
|
|
3
|
+
# Called by Claude Code after every tool use with JSON on stdin
|
|
4
|
+
#
|
|
5
|
+
# This hook receives PostToolUse event data from Claude Code and writes
|
|
6
|
+
# rich heartbeat information to enable real-time agent monitoring.
|
|
7
|
+
|
|
8
|
+
# Don't use set -e - we want the hook to be resilient to failures
|
|
9
|
+
# and never break Claude Code execution
|
|
10
|
+
|
|
11
|
+
# Parse tool info from stdin
|
|
12
|
+
TOOL_INFO=$(cat 2>/dev/null || echo '{}')
|
|
13
|
+
|
|
14
|
+
# Check if jq is available
|
|
15
|
+
if ! command -v jq &> /dev/null; then
|
|
16
|
+
echo "Warning: jq not found. Heartbeat hook requires jq to parse tool data." >&2
|
|
17
|
+
exit 0 # Silent failure - don't break Claude Code execution
|
|
18
|
+
fi
|
|
19
|
+
|
|
20
|
+
# Extract tool name and input (truncate input to 100 chars to avoid huge files)
|
|
21
|
+
TOOL_NAME=$(echo "$TOOL_INFO" | jq -r '.tool_name // "unknown"' 2>/dev/null || echo "unknown")
|
|
22
|
+
TOOL_INPUT=$(echo "$TOOL_INFO" | jq -r '.tool_input | tostring | .[0:100] // ""' 2>/dev/null || echo "")
|
|
23
|
+
|
|
24
|
+
# Get agent ID from env (set by pan work issue) or tmux session name
|
|
25
|
+
# Only use tmux session name if we're actually INSIDE a tmux session ($TMUX is set)
|
|
26
|
+
if [ -n "$PANOPTICON_AGENT_ID" ]; then
|
|
27
|
+
AGENT_ID="$PANOPTICON_AGENT_ID"
|
|
28
|
+
elif [ -n "$TMUX" ]; then
|
|
29
|
+
AGENT_ID=$(tmux display-message -p '#S' 2>/dev/null)
|
|
30
|
+
else
|
|
31
|
+
AGENT_ID="main-cli"
|
|
32
|
+
fi
|
|
33
|
+
AGENT_ID="${AGENT_ID:-unknown}"
|
|
34
|
+
|
|
35
|
+
# Get current beads task from cache (if exists)
|
|
36
|
+
TASK_CACHE="$HOME/.panopticon/agents/$AGENT_ID/current-task.json"
|
|
37
|
+
CURRENT_TASK=""
|
|
38
|
+
if [ -f "$TASK_CACHE" ]; then
|
|
39
|
+
CURRENT_TASK=$(jq -r '.title // ""' "$TASK_CACHE" 2>/dev/null || echo "")
|
|
40
|
+
fi
|
|
41
|
+
|
|
42
|
+
# Get git branch (fast, single command) - suppress all errors
|
|
43
|
+
GIT_BRANCH=$(git branch --show-current 2>/dev/null || true)
|
|
44
|
+
GIT_BRANCH="${GIT_BRANCH:-unknown}"
|
|
45
|
+
|
|
46
|
+
# Get workspace from pwd
|
|
47
|
+
WORKSPACE=$(pwd)
|
|
48
|
+
|
|
49
|
+
# Ensure heartbeat directory exists
|
|
50
|
+
HEARTBEAT_DIR="$HOME/.panopticon/heartbeats"
|
|
51
|
+
mkdir -p "$HEARTBEAT_DIR"
|
|
52
|
+
|
|
53
|
+
# Ensure agent state directory exists for activity log
|
|
54
|
+
AGENT_DIR="$HOME/.panopticon/agents/$AGENT_ID"
|
|
55
|
+
mkdir -p "$AGENT_DIR"
|
|
56
|
+
|
|
57
|
+
# Write heartbeat atomically (write to temp file, then move)
|
|
58
|
+
# Use jq to properly escape all strings in the JSON output
|
|
59
|
+
TEMP_FILE="$HEARTBEAT_DIR/$AGENT_ID.json.tmp"
|
|
60
|
+
jq -n \
|
|
61
|
+
--arg timestamp "$(date -Iseconds)" \
|
|
62
|
+
--arg agent_id "$AGENT_ID" \
|
|
63
|
+
--arg tool_name "$TOOL_NAME" \
|
|
64
|
+
--arg last_action "$TOOL_INPUT" \
|
|
65
|
+
--arg current_task "$CURRENT_TASK" \
|
|
66
|
+
--arg git_branch "$GIT_BRANCH" \
|
|
67
|
+
--arg workspace "$WORKSPACE" \
|
|
68
|
+
--argjson pid $$ \
|
|
69
|
+
'{
|
|
70
|
+
timestamp: $timestamp,
|
|
71
|
+
agent_id: $agent_id,
|
|
72
|
+
tool_name: $tool_name,
|
|
73
|
+
last_action: $last_action,
|
|
74
|
+
current_task: $current_task,
|
|
75
|
+
git_branch: $git_branch,
|
|
76
|
+
workspace: $workspace,
|
|
77
|
+
pid: $pid
|
|
78
|
+
}' > "$TEMP_FILE" 2>/dev/null || true
|
|
79
|
+
|
|
80
|
+
# Atomic move to avoid race conditions
|
|
81
|
+
mv "$TEMP_FILE" "$HEARTBEAT_DIR/$AGENT_ID.json" 2>/dev/null || true
|
|
82
|
+
|
|
83
|
+
# Append to activity log (JSONL format)
|
|
84
|
+
ACTIVITY_FILE="$AGENT_DIR/activity.jsonl"
|
|
85
|
+
|
|
86
|
+
# Create activity entry
|
|
87
|
+
ACTIVITY_ENTRY=$(jq -n \
|
|
88
|
+
--arg ts "$(date -Iseconds)" \
|
|
89
|
+
--arg tool "$TOOL_NAME" \
|
|
90
|
+
--arg action "$TOOL_INPUT" \
|
|
91
|
+
'{
|
|
92
|
+
ts: $ts,
|
|
93
|
+
tool: $tool,
|
|
94
|
+
action: $action
|
|
95
|
+
}' 2>/dev/null || echo "")
|
|
96
|
+
|
|
97
|
+
# Append to activity log if we successfully created the entry
|
|
98
|
+
if [ -n "$ACTIVITY_ENTRY" ]; then
|
|
99
|
+
echo "$ACTIVITY_ENTRY" >> "$ACTIVITY_FILE" 2>/dev/null || true
|
|
100
|
+
|
|
101
|
+
# Prune to last 100 entries (use tail for atomic operation)
|
|
102
|
+
if [ -f "$ACTIVITY_FILE" ]; then
|
|
103
|
+
TEMP_ACTIVITY="$ACTIVITY_FILE.tmp"
|
|
104
|
+
tail -n 100 "$ACTIVITY_FILE" > "$TEMP_ACTIVITY" 2>/dev/null && mv "$TEMP_ACTIVITY" "$ACTIVITY_FILE" 2>/dev/null || true
|
|
105
|
+
fi
|
|
106
|
+
fi
|
|
107
|
+
|
|
108
|
+
# Update agent runtime state with latest activity timestamp (PAN-119)
|
|
109
|
+
STATE_FILE="$AGENT_DIR/state.json"
|
|
110
|
+
TIMESTAMP="$(date -Iseconds)"
|
|
111
|
+
|
|
112
|
+
if [ -f "$STATE_FILE" ]; then
|
|
113
|
+
# Update existing state file's lastActivity field
|
|
114
|
+
TEMP_STATE="$STATE_FILE.tmp"
|
|
115
|
+
jq --arg ts "$TIMESTAMP" --arg tool "$TOOL_NAME" \
|
|
116
|
+
'.lastActivity = $ts | .currentTool = $tool | .state = "active"' \
|
|
117
|
+
"$STATE_FILE" > "$TEMP_STATE" 2>/dev/null && mv "$TEMP_STATE" "$STATE_FILE" 2>/dev/null || true
|
|
118
|
+
else
|
|
119
|
+
# Create initial state file
|
|
120
|
+
jq -n \
|
|
121
|
+
--arg ts "$TIMESTAMP" \
|
|
122
|
+
--arg tool "$TOOL_NAME" \
|
|
123
|
+
'{
|
|
124
|
+
state: "active",
|
|
125
|
+
lastActivity: $ts,
|
|
126
|
+
currentTool: $tool
|
|
127
|
+
}' > "$STATE_FILE" 2>/dev/null || true
|
|
128
|
+
fi
|
|
129
|
+
|
|
130
|
+
# Record cost event from tool usage (PAN-81)
|
|
131
|
+
# Find the record-cost-event.js script - check multiple locations
|
|
132
|
+
COST_SCRIPT=""
|
|
133
|
+
if [ -f "$HOME/.panopticon/bin/record-cost-event.js" ]; then
|
|
134
|
+
COST_SCRIPT="$HOME/.panopticon/bin/record-cost-event.js"
|
|
135
|
+
elif [ -f "$(dirname "$0")/record-cost-event.js" ]; then
|
|
136
|
+
COST_SCRIPT="$(dirname "$0")/record-cost-event.js"
|
|
137
|
+
fi
|
|
138
|
+
|
|
139
|
+
if [ -n "$COST_SCRIPT" ] && [ -f "$COST_SCRIPT" ]; then
|
|
140
|
+
# Export issue context for the cost recording script
|
|
141
|
+
export PANOPTICON_ISSUE_ID="${PANOPTICON_ISSUE_ID:-UNKNOWN}"
|
|
142
|
+
export PANOPTICON_SESSION_TYPE="${PANOPTICON_SESSION_TYPE:-implementation}"
|
|
143
|
+
|
|
144
|
+
# Call cost recording script with tool info on stdin
|
|
145
|
+
echo "$TOOL_INFO" | node "$COST_SCRIPT" 2>/dev/null || true
|
|
146
|
+
fi
|
|
147
|
+
|
|
148
|
+
# Always exit successfully - never break Claude Code execution
|
|
149
|
+
exit 0
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
#
|
|
3
|
+
# Install Panopticon git hooks in a project's repos
|
|
4
|
+
# Usage: ./install-git-hooks.sh /path/to/project
|
|
5
|
+
#
|
|
6
|
+
# For poly-repos, this will find all .git directories and install hooks in each.
|
|
7
|
+
|
|
8
|
+
set -e
|
|
9
|
+
|
|
10
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
11
|
+
HOOKS_DIR="$SCRIPT_DIR/git-hooks"
|
|
12
|
+
TARGET_DIR="${1:-.}"
|
|
13
|
+
|
|
14
|
+
if [ ! -d "$TARGET_DIR" ]; then
|
|
15
|
+
echo "Error: Directory does not exist: $TARGET_DIR"
|
|
16
|
+
exit 1
|
|
17
|
+
fi
|
|
18
|
+
|
|
19
|
+
echo "Installing Panopticon git hooks in: $TARGET_DIR"
|
|
20
|
+
echo ""
|
|
21
|
+
|
|
22
|
+
# Find all .git directories (not files - those are worktrees)
|
|
23
|
+
# Also exclude node_modules, target, etc.
|
|
24
|
+
find "$TARGET_DIR" -maxdepth 4 -type d -name ".git" \
|
|
25
|
+
-not -path "*/node_modules/*" \
|
|
26
|
+
-not -path "*/target/*" \
|
|
27
|
+
-not -path "*/.git/*" \
|
|
28
|
+
-not -path "*/workspaces/*" \
|
|
29
|
+
2>/dev/null | while read git_dir; do
|
|
30
|
+
|
|
31
|
+
hooks_target="$git_dir/hooks"
|
|
32
|
+
repo_dir="$(dirname "$git_dir")"
|
|
33
|
+
|
|
34
|
+
echo "Installing hooks in: $repo_dir"
|
|
35
|
+
|
|
36
|
+
# Create hooks directory if it doesn't exist
|
|
37
|
+
mkdir -p "$hooks_target"
|
|
38
|
+
|
|
39
|
+
# Install each hook
|
|
40
|
+
for hook in "$HOOKS_DIR"/*; do
|
|
41
|
+
if [ -f "$hook" ]; then
|
|
42
|
+
hook_name=$(basename "$hook")
|
|
43
|
+
target_hook="$hooks_target/$hook_name"
|
|
44
|
+
|
|
45
|
+
# Check if hook already exists
|
|
46
|
+
if [ -f "$target_hook" ] && [ ! -L "$target_hook" ]; then
|
|
47
|
+
echo " ⚠️ $hook_name: existing hook found, creating backup"
|
|
48
|
+
mv "$target_hook" "$target_hook.backup"
|
|
49
|
+
fi
|
|
50
|
+
|
|
51
|
+
# Create symlink to our hook
|
|
52
|
+
ln -sf "$hook" "$target_hook"
|
|
53
|
+
echo " ✓ $hook_name installed"
|
|
54
|
+
fi
|
|
55
|
+
done
|
|
56
|
+
|
|
57
|
+
echo ""
|
|
58
|
+
done
|
|
59
|
+
|
|
60
|
+
echo "Done! Git hooks installed."
|
|
61
|
+
echo ""
|
|
62
|
+
echo "The post-checkout hook will warn if the main project directory"
|
|
63
|
+
echo "is checked out to a branch other than 'main'."
|
|
64
|
+
echo ""
|
|
65
|
+
echo "To enable auto-revert (automatically switch back to main):"
|
|
66
|
+
echo " export PANOPTICON_AUTO_REVERT_CHECKOUT=1"
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# notify-complete: Send desktop notification when agent completes work
|
|
3
|
+
# Part of Panopticon - works on WSL2/Windows, Linux, and macOS
|
|
4
|
+
#
|
|
5
|
+
# Usage: notify-complete <issue-id> <title> [mr-url]
|
|
6
|
+
#
|
|
7
|
+
# Examples:
|
|
8
|
+
# notify-complete MIN-665 "Fixed login button"
|
|
9
|
+
# notify-complete PAN-96 "Implemented templates" "https://gitlab.com/mr/123"
|
|
10
|
+
|
|
11
|
+
ISSUE_ID="${1:-UNKNOWN}"
|
|
12
|
+
TITLE="${2:-Agent completed work}"
|
|
13
|
+
MR_URL="${3:-}"
|
|
14
|
+
|
|
15
|
+
# Log to completion file
|
|
16
|
+
COMPLETION_LOG="$HOME/.panopticon/agent-completed.log"
|
|
17
|
+
mkdir -p "$(dirname "$COMPLETION_LOG")"
|
|
18
|
+
echo "$(date '+%Y-%m-%d %H:%M:%S') | ${ISSUE_ID} | ${TITLE} | ${MR_URL}" >> "$COMPLETION_LOG"
|
|
19
|
+
|
|
20
|
+
# Detect platform and send notification
|
|
21
|
+
send_notification() {
|
|
22
|
+
local title="$1"
|
|
23
|
+
local message="$2"
|
|
24
|
+
|
|
25
|
+
# WSL2/Windows - use PowerShell toast notifications
|
|
26
|
+
if command -v powershell.exe &>/dev/null; then
|
|
27
|
+
powershell.exe -Command "
|
|
28
|
+
\$xml = @\"
|
|
29
|
+
<toast>
|
|
30
|
+
<visual>
|
|
31
|
+
<binding template=\"ToastText02\">
|
|
32
|
+
<text id=\"1\">${title}</text>
|
|
33
|
+
<text id=\"2\">${message}</text>
|
|
34
|
+
</binding>
|
|
35
|
+
</visual>
|
|
36
|
+
<audio src=\"ms-winsoundevent:Notification.Default\"/>
|
|
37
|
+
</toast>
|
|
38
|
+
\"@
|
|
39
|
+
|
|
40
|
+
[Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime] | Out-Null
|
|
41
|
+
[Windows.Data.Xml.Dom.XmlDocument, Windows.Data.Xml.Dom.XmlDocument, ContentType = WindowsRuntime] | Out-Null
|
|
42
|
+
|
|
43
|
+
\$template = New-Object Windows.Data.Xml.Dom.XmlDocument
|
|
44
|
+
\$template.LoadXml(\$xml)
|
|
45
|
+
\$toast = New-Object Windows.UI.Notifications.ToastNotification \$template
|
|
46
|
+
[Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier('Panopticon').Show(\$toast)
|
|
47
|
+
" 2>/dev/null && return 0
|
|
48
|
+
|
|
49
|
+
# Fallback: MessageBox
|
|
50
|
+
powershell.exe -Command "[System.Reflection.Assembly]::LoadWithPartialName('System.Windows.Forms'); [System.Windows.Forms.MessageBox]::Show('${message}', '${title}', 'OK', 'Information')" 2>/dev/null &
|
|
51
|
+
return 0
|
|
52
|
+
fi
|
|
53
|
+
|
|
54
|
+
# macOS - use osascript
|
|
55
|
+
if command -v osascript &>/dev/null; then
|
|
56
|
+
osascript -e "display notification \"${message}\" with title \"${title}\"" 2>/dev/null
|
|
57
|
+
return 0
|
|
58
|
+
fi
|
|
59
|
+
|
|
60
|
+
# Linux - use notify-send
|
|
61
|
+
if command -v notify-send &>/dev/null; then
|
|
62
|
+
notify-send "${title}" "${message}" 2>/dev/null
|
|
63
|
+
return 0
|
|
64
|
+
fi
|
|
65
|
+
|
|
66
|
+
# Fallback - just print to console
|
|
67
|
+
echo "[NOTIFICATION] ${title}: ${message}"
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
# Build notification message
|
|
71
|
+
if [ -n "$MR_URL" ]; then
|
|
72
|
+
MESSAGE="${TITLE} - Ready for review"
|
|
73
|
+
else
|
|
74
|
+
MESSAGE="${TITLE} - Work complete"
|
|
75
|
+
fi
|
|
76
|
+
|
|
77
|
+
send_notification "Panopticon: ${ISSUE_ID}" "$MESSAGE"
|
|
78
|
+
|
|
79
|
+
echo "Notification sent for ${ISSUE_ID}"
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Postinstall script for Panopticon
|
|
4
|
+
*
|
|
5
|
+
* Automatically syncs hooks after npm install/upgrade if Panopticon
|
|
6
|
+
* has been initialized (bin dir exists).
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { existsSync, readdirSync, copyFileSync, chmodSync, mkdirSync } from 'fs';
|
|
10
|
+
import { join, dirname } from 'path';
|
|
11
|
+
import { homedir } from 'os';
|
|
12
|
+
import { fileURLToPath } from 'url';
|
|
13
|
+
|
|
14
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
15
|
+
const BIN_DIR = join(homedir(), '.panopticon', 'bin');
|
|
16
|
+
const SCRIPTS_DIR = __dirname;
|
|
17
|
+
|
|
18
|
+
// Only run if Panopticon has been initialized
|
|
19
|
+
if (!existsSync(join(homedir(), '.panopticon'))) {
|
|
20
|
+
console.log('Panopticon not initialized yet. Run `pan init` to set up.');
|
|
21
|
+
process.exit(0);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Ensure bin directory exists
|
|
25
|
+
mkdirSync(BIN_DIR, { recursive: true });
|
|
26
|
+
|
|
27
|
+
// Copy all scripts from scripts/ to ~/.panopticon/bin/
|
|
28
|
+
const scripts = readdirSync(SCRIPTS_DIR)
|
|
29
|
+
.filter(f => !f.startsWith('.') && !f.endsWith('.mjs') && !f.endsWith('.js'));
|
|
30
|
+
|
|
31
|
+
let synced = 0;
|
|
32
|
+
for (const script of scripts) {
|
|
33
|
+
try {
|
|
34
|
+
const source = join(SCRIPTS_DIR, script);
|
|
35
|
+
const target = join(BIN_DIR, script);
|
|
36
|
+
copyFileSync(source, target);
|
|
37
|
+
chmodSync(target, 0o755);
|
|
38
|
+
synced++;
|
|
39
|
+
} catch (e) {
|
|
40
|
+
// Ignore errors, hooks are non-critical
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (synced > 0) {
|
|
45
|
+
console.log(`✓ Synced ${synced} hooks to ~/.panopticon/bin/`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Suggest running full sync
|
|
49
|
+
console.log('Run `pan sync` to sync skills and commands to AI tools.');
|