claudenv 1.2.4 → 1.3.0

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/src/memory.js ADDED
@@ -0,0 +1,111 @@
1
+ /**
2
+ * CLI: `claudenv memory init/index/show/edit`
3
+ *
4
+ * Owns the user-facing surface for the global ~/.claudenv/memories/ layout.
5
+ * Reuses regen-index for `index` so there's one canonical regeneration path.
6
+ */
7
+
8
+ import { mkdir, readFile, writeFile, stat } from 'node:fs/promises';
9
+ import { join, dirname, resolve } from 'node:path';
10
+ import { spawnSync } from 'node:child_process';
11
+ import {
12
+ claudenvHome,
13
+ memoriesDir,
14
+ globalDecisionsDir,
15
+ canonIndexPath,
16
+ userPrefsPath,
17
+ indexMdPath,
18
+ } from './memory-paths.js';
19
+ import { regenIndex } from './hooks/regen-index.js';
20
+
21
+ /**
22
+ * `claudenv memory init` — create the global memory layout if absent.
23
+ * Idempotent; never overwrites existing files.
24
+ */
25
+ export async function memoryInit() {
26
+ const created = [];
27
+ const skipped = [];
28
+
29
+ // Layout
30
+ const dirs = [
31
+ memoriesDir(),
32
+ globalDecisionsDir(),
33
+ join(memoriesDir(), 'canon'),
34
+ join(memoriesDir(), 'user'),
35
+ ];
36
+ for (const d of dirs) {
37
+ await mkdir(d, { recursive: true });
38
+ }
39
+
40
+ // Seed files
41
+ const seeds = [
42
+ {
43
+ path: indexMdPath(),
44
+ content:
45
+ '# claudenv memory — INDEX\n\n> Auto-generated by `claudenv memory index`. Hand-edits will be overwritten.\n\n## Recent decisions\n\n_No decisions yet — vibe-decisions will log here._\n',
46
+ },
47
+ {
48
+ path: canonIndexPath(),
49
+ content:
50
+ '# Личный канон. Добавляйте через: claudenv canon add <topic> <url> --why "<reason>"\n',
51
+ },
52
+ {
53
+ path: join(claudenvHome(), '.gitignore'),
54
+ content: 'sessions/\n*.log\n.log/\nkeys/private*\n*.local.*\n*.secret.*\n',
55
+ },
56
+ ];
57
+
58
+ for (const { path, content } of seeds) {
59
+ try {
60
+ await stat(path);
61
+ skipped.push(path);
62
+ } catch {
63
+ await mkdir(dirname(path), { recursive: true });
64
+ await writeFile(path, content, 'utf-8');
65
+ created.push(path);
66
+ }
67
+ }
68
+
69
+ return { created, skipped };
70
+ }
71
+
72
+ /**
73
+ * `claudenv memory index` — regenerate INDEX.md from current decisions + prefs.
74
+ */
75
+ export async function memoryIndex({ cwd } = {}) {
76
+ return await regenIndex({ cwd: cwd || process.cwd() });
77
+ }
78
+
79
+ /**
80
+ * `claudenv memory show <relpath>` — print a file from memories/.
81
+ * Path is interpreted relative to ~/.claudenv/memories/.
82
+ */
83
+ export async function memoryShow(relPath) {
84
+ if (!relPath) throw new Error('Usage: claudenv memory show <path>');
85
+ const full = resolve(memoriesDir(), relPath);
86
+ if (!full.startsWith(memoriesDir())) {
87
+ throw new Error('Path escapes memories root');
88
+ }
89
+ return await readFile(full, 'utf-8');
90
+ }
91
+
92
+ /**
93
+ * `claudenv memory edit <relpath>` — open a memory file in $EDITOR (or vi).
94
+ * Creates the file if missing.
95
+ */
96
+ export async function memoryEdit(relPath) {
97
+ if (!relPath) throw new Error('Usage: claudenv memory edit <path>');
98
+ const full = resolve(memoriesDir(), relPath);
99
+ if (!full.startsWith(memoriesDir())) {
100
+ throw new Error('Path escapes memories root');
101
+ }
102
+ await mkdir(dirname(full), { recursive: true });
103
+ try {
104
+ await stat(full);
105
+ } catch {
106
+ await writeFile(full, '', 'utf-8');
107
+ }
108
+ const editor = process.env.EDITOR || 'vi';
109
+ const result = spawnSync(editor, [full], { stdio: 'inherit' });
110
+ return { path: full, exitCode: result.status ?? 0 };
111
+ }
package/src/profiles.js CHANGED
@@ -18,6 +18,7 @@ export const AUTONOMY_PROFILES = {
18
18
  safe: {
19
19
  name: 'safe',
20
20
  description: 'Read-only + limited bash — safe for exploration',
21
+ model: 'sonnet',
21
22
  allowedTools: [
22
23
  'Read',
23
24
  'Glob',
@@ -41,6 +42,7 @@ export const AUTONOMY_PROFILES = {
41
42
  moderate: {
42
43
  name: 'moderate',
43
44
  description: 'Full development with deny-list — safe for most development work',
45
+ model: 'sonnet',
44
46
  allowedTools: [
45
47
  'Read',
46
48
  'Edit',
@@ -72,6 +74,7 @@ export const AUTONOMY_PROFILES = {
72
74
  full: {
73
75
  name: 'full',
74
76
  description: 'Full autonomy — unrestricted access with audit logging',
77
+ model: 'opus',
75
78
  allowedTools: [],
76
79
  disallowedTools: [],
77
80
  skipPermissions: true,
@@ -81,6 +84,7 @@ export const AUTONOMY_PROFILES = {
81
84
  ci: {
82
85
  name: 'ci',
83
86
  description: 'Headless CI/CD mode — full autonomy with turn/budget limits',
87
+ model: 'haiku',
84
88
  allowedTools: [],
85
89
  disallowedTools: [],
86
90
  skipPermissions: true,
package/src/report.js ADDED
@@ -0,0 +1,160 @@
1
+ // =============================================
2
+ // Work report reader/formatter/watcher
3
+ // =============================================
4
+
5
+ import { readFile, writeFile, mkdir } from 'node:fs/promises';
6
+ import { createReadStream, watch, statSync } from 'node:fs';
7
+ import { join } from 'node:path';
8
+
9
+ const REPORT_FILE = '.claude/work-report.jsonl';
10
+
11
+ /**
12
+ * Read all report events from the JSONL file.
13
+ * @param {string} cwd - Working directory
14
+ * @returns {Promise<Array<object>>}
15
+ */
16
+ export async function readReport(cwd) {
17
+ try {
18
+ const content = await readFile(join(cwd, REPORT_FILE), 'utf-8');
19
+ return content
20
+ .split('\n')
21
+ .filter((line) => line.trim())
22
+ .map((line) => {
23
+ try {
24
+ return JSON.parse(line);
25
+ } catch {
26
+ return null;
27
+ }
28
+ })
29
+ .filter(Boolean);
30
+ } catch {
31
+ return [];
32
+ }
33
+ }
34
+
35
+ /**
36
+ * Format report events as a human-readable timeline.
37
+ * @param {Array<object>} events
38
+ * @returns {string}
39
+ */
40
+ export function formatReport(events) {
41
+ if (events.length === 0) return ' No report events found.\n';
42
+
43
+ const lines = [];
44
+ lines.push(' Work Report');
45
+ lines.push(` ${'─'.repeat(50)}`);
46
+
47
+ for (const ev of events) {
48
+ const time = ev.ts ? new Date(ev.ts).toLocaleTimeString() : '??:??:??';
49
+ switch (ev.event) {
50
+ case 'loop_start':
51
+ lines.push(` ${time} LOOP START`);
52
+ lines.push(` Goal: ${ev.goal || 'General improvement'}`);
53
+ if (ev.model) lines.push(` Model: ${ev.model}`);
54
+ if (ev.gitTag) lines.push(` Tag: ${ev.gitTag}`);
55
+ break;
56
+
57
+ case 'iteration_start':
58
+ lines.push(` ${time} ITERATION ${ev.iteration} start${ev.type ? ` (${ev.type})` : ''}`);
59
+ break;
60
+
61
+ case 'iteration_end':
62
+ lines.push(` ${time} ITERATION ${ev.iteration} done`);
63
+ if (ev.summary) lines.push(` ${ev.summary.slice(0, 120)}`);
64
+ if (ev.commitHash) lines.push(` Commit: ${ev.commitHash}`);
65
+ if (ev.tokens) lines.push(` Tokens: ${ev.tokens.in || 0} in / ${ev.tokens.out || 0} out`);
66
+ break;
67
+
68
+ case 'rate_limit':
69
+ lines.push(` ${time} RATE LIMITED at iteration ${ev.iteration}`);
70
+ if (ev.message) lines.push(` ${ev.message}`);
71
+ break;
72
+
73
+ case 'loop_end':
74
+ lines.push(` ${time} LOOP END — ${ev.reason || 'unknown'} (${ev.totalIterations || '?'} iterations)`);
75
+ break;
76
+
77
+ default:
78
+ lines.push(` ${time} ${ev.event || 'unknown'}`);
79
+ }
80
+ lines.push('');
81
+ }
82
+
83
+ lines.push(` ${'─'.repeat(50)}`);
84
+ return lines.join('\n') + '\n';
85
+ }
86
+
87
+ /**
88
+ * Format a single event as a compact one-line string (for follow/watch mode).
89
+ * @param {object} ev
90
+ * @returns {string}
91
+ */
92
+ export function formatEventLine(ev) {
93
+ const time = ev.ts ? new Date(ev.ts).toLocaleTimeString() : '??:??:??';
94
+ switch (ev.event) {
95
+ case 'loop_start':
96
+ return ` ${time} LOOP START — ${ev.goal || 'General improvement'}${ev.model ? ` [${ev.model}]` : ''}\n`;
97
+ case 'iteration_start':
98
+ return ` ${time} ITERATION ${ev.iteration} start${ev.type ? ` (${ev.type})` : ''}\n`;
99
+ case 'iteration_end': {
100
+ let line = ` ${time} ITERATION ${ev.iteration} done`;
101
+ if (ev.commitHash) line += ` [${ev.commitHash}]`;
102
+ if (ev.tokens) line += ` (${ev.tokens.in || 0}/${ev.tokens.out || 0} tokens)`;
103
+ line += '\n';
104
+ if (ev.summary) line += ` ${ev.summary.slice(0, 120)}\n`;
105
+ return line;
106
+ }
107
+ case 'rate_limit':
108
+ return ` ${time} RATE LIMITED at iteration ${ev.iteration}${ev.message ? ` — ${ev.message}` : ''}\n`;
109
+ case 'loop_end':
110
+ return ` ${time} LOOP END — ${ev.reason || 'unknown'} (${ev.totalIterations || '?'} iterations)\n`;
111
+ default:
112
+ return ` ${time} ${ev.event || 'unknown'}\n`;
113
+ }
114
+ }
115
+
116
+ /**
117
+ * Watch the report file for new events and call cb for each.
118
+ * @param {string} cwd - Working directory
119
+ * @param {function} cb - Callback receiving each new event object
120
+ * @returns {{ close: function }} Watcher handle
121
+ */
122
+ export async function watchReport(cwd, cb) {
123
+ const filePath = join(cwd, REPORT_FILE);
124
+ let position = 0;
125
+
126
+ // Ensure the file exists (watch throws on non-existent files)
127
+ try {
128
+ const { size } = statSync(filePath);
129
+ position = size;
130
+ } catch {
131
+ await mkdir(join(cwd, '.claude'), { recursive: true });
132
+ await writeFile(filePath, '');
133
+ }
134
+
135
+ const watcher = watch(filePath, () => {
136
+ // On change, read new lines from last position
137
+ const stream = createReadStream(filePath, { start: position, encoding: 'utf-8' });
138
+ let buffer = '';
139
+
140
+ stream.on('data', (chunk) => {
141
+ buffer += chunk;
142
+ position += Buffer.byteLength(chunk, 'utf-8');
143
+ });
144
+
145
+ stream.on('end', () => {
146
+ const lines = buffer.split('\n').filter((l) => l.trim());
147
+ for (const line of lines) {
148
+ try {
149
+ cb(JSON.parse(line));
150
+ } catch {
151
+ // skip malformed lines
152
+ }
153
+ }
154
+ });
155
+ });
156
+
157
+ return {
158
+ close: () => watcher.close(),
159
+ };
160
+ }