deepflow 0.1.87 → 0.1.89

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.
@@ -75,11 +75,14 @@ function buildContextMeter(contextWindow, data) {
75
75
  percentage = Math.min(100, Math.round(percentage));
76
76
 
77
77
  // Write context usage to file for deepflow commands
78
- writeContextUsage(percentage);
78
+ writeContextUsage(percentage, data);
79
79
 
80
80
  // Write token history for instrumentation
81
81
  writeTokenHistory(contextWindow, data);
82
82
 
83
+ // Write cache history for cross-session persistence
84
+ writeCacheHistory(contextWindow, data);
85
+
83
86
  // Build 10-segment bar
84
87
  const segments = 10;
85
88
  const filled = Math.round((percentage / 100) * segments);
@@ -112,9 +115,10 @@ function checkForUpdate() {
112
115
  return null;
113
116
  }
114
117
 
115
- function writeContextUsage(percentage) {
118
+ function writeContextUsage(percentage, data) {
116
119
  try {
117
- const deepflowDir = path.join(process.cwd(), '.deepflow');
120
+ const baseDir = data?.workspace?.current_dir || process.cwd();
121
+ const deepflowDir = path.join(baseDir, '.deepflow');
118
122
  if (!fs.existsSync(deepflowDir)) {
119
123
  fs.mkdirSync(deepflowDir, { recursive: true });
120
124
  }
@@ -130,7 +134,8 @@ function writeContextUsage(percentage) {
130
134
 
131
135
  function writeTokenHistory(contextWindow, data) {
132
136
  try {
133
- const deepflowDir = path.join(process.cwd(), '.deepflow');
137
+ const baseDir = data?.workspace?.current_dir || process.cwd();
138
+ const deepflowDir = path.join(baseDir, '.deepflow');
134
139
  if (!fs.existsSync(deepflowDir)) {
135
140
  fs.mkdirSync(deepflowDir, { recursive: true });
136
141
  }
@@ -142,6 +147,9 @@ function writeTokenHistory(contextWindow, data) {
142
147
  const contextWindowSize = contextWindow.context_window_size || 0;
143
148
  const usedPercentage = contextWindow.used_percentage || 0;
144
149
 
150
+ const agentRole = process.env.DEEPFLOW_AGENT_ROLE || 'orchestrator';
151
+ const taskId = process.env.DEEPFLOW_TASK_ID || null;
152
+
145
153
  const record = {
146
154
  timestamp,
147
155
  input_tokens: usage.input_tokens || 0,
@@ -150,7 +158,9 @@ function writeTokenHistory(contextWindow, data) {
150
158
  context_window_size: contextWindowSize,
151
159
  used_percentage: usedPercentage,
152
160
  model,
153
- session_id: sessionId
161
+ session_id: sessionId,
162
+ agent_role: agentRole,
163
+ task_id: taskId
154
164
  };
155
165
 
156
166
  const tokenHistoryPath = path.join(deepflowDir, 'token-history.jsonl');
@@ -159,3 +169,65 @@ function writeTokenHistory(contextWindow, data) {
159
169
  // Fail silently
160
170
  }
161
171
  }
172
+
173
+ function writeCacheHistory(contextWindow, data) {
174
+ try {
175
+ const usage = contextWindow.current_usage || {};
176
+ const sessionId = data.session_id || 'unknown';
177
+
178
+ const inputTokens = usage.input_tokens || 0;
179
+ const cacheCreationTokens = usage.cache_creation_input_tokens || 0;
180
+ const cacheReadTokens = usage.cache_read_input_tokens || 0;
181
+ const totalTokens = inputTokens + cacheCreationTokens + cacheReadTokens;
182
+
183
+ // Compute cache hit ratio: cache_read / total
184
+ const cacheHitRatio = totalTokens > 0 ? cacheReadTokens / totalTokens : 0;
185
+
186
+ const model = data.model?.id || data.model?.display_name || 'unknown';
187
+ const agentRole = process.env.DEEPFLOW_AGENT_ROLE || 'orchestrator';
188
+ const taskId = process.env.DEEPFLOW_TASK_ID || null;
189
+
190
+ const cacheHistoryPath = path.join(os.homedir(), '.claude', 'cache-history.jsonl');
191
+
192
+ // Dedup: only write if session_id differs from last written record
193
+ let lastSessionId = null;
194
+ if (fs.existsSync(cacheHistoryPath)) {
195
+ const content = fs.readFileSync(cacheHistoryPath, 'utf8');
196
+ const lines = content.trimEnd().split('\n');
197
+ if (lines.length > 0) {
198
+ try {
199
+ const lastRecord = JSON.parse(lines[lines.length - 1]);
200
+ lastSessionId = lastRecord.session_id;
201
+ } catch (e) {
202
+ // Ignore parse errors on last line
203
+ }
204
+ }
205
+ }
206
+
207
+ if (sessionId === lastSessionId) {
208
+ return;
209
+ }
210
+
211
+ const record = {
212
+ timestamp: new Date().toISOString(),
213
+ session_id: sessionId,
214
+ cache_hit_ratio: Math.round(cacheHitRatio * 10000) / 10000,
215
+ total_tokens: totalTokens,
216
+ agent_breakdown: {
217
+ agent_role: agentRole,
218
+ task_id: taskId,
219
+ model
220
+ }
221
+ };
222
+
223
+ // Ensure ~/.claude directory exists
224
+ const claudeDir = path.join(os.homedir(), '.claude');
225
+ if (!fs.existsSync(claudeDir)) {
226
+ fs.mkdirSync(claudeDir, { recursive: true });
227
+ }
228
+
229
+ fs.appendFileSync(cacheHistoryPath, JSON.stringify(record) + '\n');
230
+ } catch (e) {
231
+ // Fail silently
232
+ }
233
+ }
@@ -0,0 +1,41 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Spike hook: capture raw PostToolUse stdin payload
4
+ * Writes the raw JSON to /tmp/df-posttooluse-payload.json for inspection.
5
+ * Safe to install temporarily — exits cleanly (code 0) always.
6
+ *
7
+ * Usage in ~/.claude/settings.json:
8
+ * "PostToolUse": [{ "hooks": [{ "type": "command", "command": "node /path/to/df-tool-usage-spike.js" }] }]
9
+ */
10
+
11
+ 'use strict';
12
+
13
+ const fs = require('fs');
14
+
15
+ let raw = '';
16
+ process.stdin.setEncoding('utf8');
17
+ process.stdin.on('data', chunk => { raw += chunk; });
18
+ process.stdin.on('end', () => {
19
+ try {
20
+ // Write raw payload for inspection
21
+ fs.writeFileSync('/tmp/df-posttooluse-payload.json', raw);
22
+
23
+ // Also append a minimal summary line for quick review
24
+ const data = JSON.parse(raw);
25
+ const summary = {
26
+ hook_event_name: data.hook_event_name,
27
+ tool_name: data.tool_name,
28
+ tool_use_id: data.tool_use_id,
29
+ session_id: data.session_id,
30
+ cwd: data.cwd,
31
+ permission_mode: data.permission_mode,
32
+ tool_input_keys: data.tool_input ? Object.keys(data.tool_input) : [],
33
+ tool_response_keys: data.tool_response ? Object.keys(data.tool_response) : [],
34
+ transcript_path: data.transcript_path,
35
+ };
36
+ fs.appendFileSync('/tmp/df-posttooluse-summary.jsonl', JSON.stringify(summary) + '\n');
37
+ } catch (_e) {
38
+ // Fail silently — never break tool execution
39
+ }
40
+ process.exit(0);
41
+ });
@@ -0,0 +1,86 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * deepflow tool usage logger
4
+ * Logs every PostToolUse event to ~/.claude/tool-usage.jsonl for token instrumentation.
5
+ * Exits silently (code 0) on all errors — never breaks tool execution.
6
+ *
7
+ * Output record fields (REQ-2):
8
+ * timestamp, session_id, tool_name, command, output_size_est_tokens,
9
+ * project, phase, task_id
10
+ */
11
+
12
+ 'use strict';
13
+
14
+ const fs = require('fs');
15
+ const path = require('path');
16
+ const os = require('os');
17
+
18
+ const TOOL_USAGE_LOG = path.join(os.homedir(), '.claude', 'tool-usage.jsonl');
19
+
20
+ /**
21
+ * Infer phase from cwd.
22
+ * If cwd contains .deepflow/worktrees/, parse the worktree dir name for phase.
23
+ * Worktree dirs are named "execute", "verify", or task-specific names.
24
+ * Default: "manual"
25
+ */
26
+ function inferPhase(cwd) {
27
+ if (!cwd) return 'manual';
28
+ const match = cwd.match(/\.deepflow[/\\]worktrees[/\\]([^/\\]+)/);
29
+ if (!match) return 'manual';
30
+ const worktreeName = match[1].toLowerCase();
31
+ if (worktreeName === 'execute') return 'execute';
32
+ if (worktreeName === 'verify') return 'verify';
33
+ // Could be a task-specific worktree — still inside worktrees/, treat as execute
34
+ return 'execute';
35
+ }
36
+
37
+ /**
38
+ * Extract task_id from worktree directory name.
39
+ * Pattern: T{n} prefix, e.g. "T3-feature" → "T3", "T12" → "T12"
40
+ * Returns null if not in a worktree or no task prefix found.
41
+ */
42
+ function extractTaskId(cwd) {
43
+ if (!cwd) return null;
44
+ const match = cwd.match(/\.deepflow[/\\]worktrees[/\\]([^/\\]+)/);
45
+ if (!match) return null;
46
+ const worktreeName = match[1];
47
+ const taskMatch = worktreeName.match(/^(T\d+)/i);
48
+ return taskMatch ? taskMatch[1].toUpperCase() : null;
49
+ }
50
+
51
+ // Read all stdin, then process
52
+ let raw = '';
53
+ process.stdin.setEncoding('utf8');
54
+ process.stdin.on('data', chunk => { raw += chunk; });
55
+ process.stdin.on('end', () => {
56
+ try {
57
+ const data = JSON.parse(raw);
58
+
59
+ const toolName = data.tool_name || null;
60
+ const toolResponse = data.tool_response;
61
+ const cwd = data.cwd || '';
62
+
63
+ const record = {
64
+ timestamp: new Date().toISOString(),
65
+ session_id: data.session_id || null,
66
+ tool_name: toolName,
67
+ command: (toolName === 'Bash' && data.tool_input && data.tool_input.command != null)
68
+ ? data.tool_input.command
69
+ : null,
70
+ output_size_est_tokens: Math.ceil(JSON.stringify(toolResponse).length / 4),
71
+ project: cwd ? path.basename(cwd) : null,
72
+ phase: inferPhase(cwd),
73
+ task_id: extractTaskId(cwd),
74
+ };
75
+
76
+ const logDir = path.dirname(TOOL_USAGE_LOG);
77
+ if (!fs.existsSync(logDir)) {
78
+ fs.mkdirSync(logDir, { recursive: true });
79
+ }
80
+
81
+ fs.appendFileSync(TOOL_USAGE_LOG, JSON.stringify(record) + '\n');
82
+ } catch (_e) {
83
+ // Fail silently — never break tool execution
84
+ }
85
+ process.exit(0);
86
+ });
@@ -0,0 +1,101 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * deepflow worktree guard
4
+ * PostToolUse hook: blocks Write/Edit to main-branch files when a df/* worktree exists.
5
+ *
6
+ * REQ-3 AC-4: exit(1) to block the tool call when all conditions hold:
7
+ * 1. tool_name is Write or Edit
8
+ * 2. current branch is main (or master)
9
+ * 3. a df/* worktree branch exists
10
+ * 4. file_path is NOT on the allowlist (.deepflow/, PLAN.md, specs/)
11
+ *
12
+ * REQ-3 AC-5: allowlisted paths always pass through (no false positives).
13
+ *
14
+ * Exits silently (code 0) on parse errors or git failures — never breaks tool
15
+ * execution in non-deepflow projects.
16
+ */
17
+
18
+ 'use strict';
19
+
20
+ const { execFileSync } = require('child_process');
21
+
22
+ // Paths that are always allowed regardless of worktree state
23
+ const ALLOWLIST = [
24
+ /(?:^|\/)\.deepflow\//,
25
+ /(?:^|\/)PLAN\.md$/,
26
+ /(?:^|\/)specs\//,
27
+ ];
28
+
29
+ function isAllowlisted(filePath) {
30
+ return ALLOWLIST.some(re => re.test(filePath));
31
+ }
32
+
33
+ function currentBranch(cwd) {
34
+ try {
35
+ return execFileSync('git', ['rev-parse', '--abbrev-ref', 'HEAD'], {
36
+ encoding: 'utf8',
37
+ cwd,
38
+ stdio: ['ignore', 'pipe', 'ignore'],
39
+ }).trim();
40
+ } catch (_) {
41
+ return null;
42
+ }
43
+ }
44
+
45
+ function dfWorktreeExists(cwd) {
46
+ try {
47
+ const out = execFileSync('git', ['branch', '--list', 'df/*'], {
48
+ encoding: 'utf8',
49
+ cwd,
50
+ stdio: ['ignore', 'pipe', 'ignore'],
51
+ });
52
+ return out.trim().length > 0;
53
+ } catch (_) {
54
+ return false;
55
+ }
56
+ }
57
+
58
+ let raw = '';
59
+ process.stdin.setEncoding('utf8');
60
+ process.stdin.on('data', chunk => { raw += chunk; });
61
+ process.stdin.on('end', () => {
62
+ try {
63
+ const data = JSON.parse(raw);
64
+ const toolName = data.tool_name || '';
65
+
66
+ // Only guard Write and Edit
67
+ if (toolName !== 'Write' && toolName !== 'Edit') {
68
+ process.exit(0);
69
+ }
70
+
71
+ const filePath = (data.tool_input && data.tool_input.file_path) || '';
72
+ const cwd = data.cwd || process.cwd();
73
+
74
+ // Allowlisted paths always pass
75
+ if (isAllowlisted(filePath)) {
76
+ process.exit(0);
77
+ }
78
+
79
+ const branch = currentBranch(cwd);
80
+
81
+ // Only guard when on main/master
82
+ if (branch !== 'main' && branch !== 'master') {
83
+ process.exit(0);
84
+ }
85
+
86
+ // Block only when a df/* worktree branch exists
87
+ if (!dfWorktreeExists(cwd)) {
88
+ process.exit(0);
89
+ }
90
+
91
+ // All conditions met — block the write
92
+ console.error(
93
+ `[df-worktree-guard] Blocked ${toolName} to "${filePath}" on main branch ` +
94
+ `while df/* worktree exists. Make changes inside the worktree branch instead.`
95
+ );
96
+ process.exit(1);
97
+ } catch (_e) {
98
+ // Parse or unexpected error — fail open so we never break non-deepflow projects
99
+ process.exit(0);
100
+ }
101
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "deepflow",
3
- "version": "0.1.87",
3
+ "version": "0.1.89",
4
4
  "description": "Doing reveals what thinking can't predict — spec-driven iterative development for Claude Code",
5
5
  "keywords": [
6
6
  "claude",