zames_pro 2.63.1 → 2.64.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/CHANGELOG.md CHANGED
@@ -5,6 +5,26 @@ All notable changes to this project are documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [Unreleased]
9
+
10
+ ## [2.64.0] - 2026-10-06
11
+
12
+ ### Added
13
+
14
+ - Генератор черновика CHANGELOG из conventional-commits (BACKLOG D2):
15
+ `npm run changelog:draft [ref]` печатает готовый блок для «Unreleased» из
16
+ коммитов после последнего тега (или указанного ref). Чистый модуль
17
+ `src/changelog.ts` (`parseConventionalCommit` / `groupCommits` /
18
+ `renderChangelogDraft`, покрыт тестами). Файл `CHANGELOG.md` НЕ
19
+ перезаписывается автоматически — черновик ревьюится вручную.
20
+
21
+ ### Changed
22
+
23
+ - Рефакторинг (BACKLOG C3, шаг 1): разрешение путей вложения и инлайн
24
+ `@file`-ссылок вынесены из `src/index.ts` в `src/attach-refs.ts`
25
+ (`resolveAttachPath` / `inlineAtRefs`) — теперь это отдельный тестируемый
26
+ модуль.
27
+
8
28
  ## [2.63.1] - 2026-10-06
9
29
 
10
30
  ### Fixed
@@ -328,7 +348,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
328
348
  - Session banner warns when no saved DeepSeek session exists.
329
349
  - `--no-color` flag (explicit `NO_COLOR`).
330
350
 
331
- [Unreleased]: https://github.com/Viqto0r/zames_pro/compare/v2.63.1...HEAD
351
+ [Unreleased]: https://github.com/Viqto0r/zames_pro/compare/v2.64.0...HEAD
352
+ [2.64.0]: https://github.com/Viqto0r/zames_pro/compare/v2.63.1...v2.64.0
332
353
  [2.63.1]: https://github.com/Viqto0r/zames_pro/compare/v2.63.0...v2.63.1
333
354
  [2.63.0]: https://github.com/Viqto0r/zames_pro/compare/v2.61.0...v2.63.0
334
355
  [2.61.0]: https://github.com/Viqto0r/zames_pro/compare/v2.60.0...v2.61.0
@@ -0,0 +1,90 @@
1
+ import fs from 'fs/promises';
2
+ import path from 'path';
3
+ import { looksLikeFilePath, isImageName } from './attachments.js';
4
+ import { extractAtFileRefs } from './path-token.js';
5
+ // Path resolution and `@file` reference inlining (BACKLOG B5, C3).
6
+ // Extracted from src/index.ts so the logic is unit-testable without the whole
7
+ // CLI. This module touches only the filesystem (no browser/terminal).
8
+ const NL = String.fromCharCode(10);
9
+ // Resolve a pasted string into a path to an EXISTING file. Handles quoted
10
+ // paths (drag&drop from some file managers adds quotes) and paths relative to
11
+ // the working directory. Returns null when nothing matches.
12
+ export async function resolveAttachPath(workdir, raw) {
13
+ let s = String(raw || '').trim();
14
+ if (!s)
15
+ return null;
16
+ if ((s.startsWith('"') && s.endsWith('"')) ||
17
+ (s.startsWith("'") && s.endsWith("'"))) {
18
+ s = s.slice(1, -1);
19
+ }
20
+ s = s.replace(/\\ /g, ' ');
21
+ if (!looksLikeFilePath(s) && !isImageName(s))
22
+ return null;
23
+ const candidates = path.isAbsolute(s)
24
+ ? [s]
25
+ : [
26
+ // Most common: relative to the project (working) directory.
27
+ path.resolve(workdir, s),
28
+ // The operator may paste a path relative to the terminal cwd (which
29
+ // can be the parent of the project) or copy it with the project name
30
+ // included, e.g. "zames_pro/tmp/image.png" while workdir is already
31
+ // .../zames_pro. Try both so the marker appears instead of raw text.
32
+ path.resolve(process.cwd(), s),
33
+ path.resolve(workdir, '..', s),
34
+ ];
35
+ for (const c of candidates) {
36
+ const st = await fs.stat(c).catch(() => null);
37
+ if (st && st.isFile())
38
+ return c;
39
+ }
40
+ return null;
41
+ }
42
+ // Per-file and total caps for `@path` inlining (B5). A reference should SAVE
43
+ // round-trips, not flood the context: a huge file is truncated with a note
44
+ // telling the agent to read the rest itself.
45
+ export const AT_REF_FILE_CAP = 60 * 1024;
46
+ export const AT_REF_TOTAL_CAP = 200 * 1024;
47
+ // Inline `@path` references found in a task text (B5). Returns the augmented
48
+ // text plus the list of files actually inlined (for the operator notice).
49
+ // Unknown/unreadable references are left in place untouched -- they may be
50
+ // part of the prose; we never fail the task over a missing file.
51
+ export async function inlineAtRefs(workdir, text) {
52
+ const refs = extractAtFileRefs(text);
53
+ if (!refs.length)
54
+ return { text, inlined: [] };
55
+ const blocks = [];
56
+ const inlined = [];
57
+ let total = 0;
58
+ for (const ref of refs) {
59
+ if (total >= AT_REF_TOTAL_CAP)
60
+ break;
61
+ const file = await resolveAttachPath(workdir, ref);
62
+ if (!file)
63
+ continue;
64
+ let raw;
65
+ try {
66
+ raw = await fs.readFile(file, 'utf-8');
67
+ }
68
+ catch {
69
+ continue;
70
+ }
71
+ let body = raw;
72
+ let note = '';
73
+ if (body.length > AT_REF_FILE_CAP) {
74
+ body = body.slice(0, AT_REF_FILE_CAP);
75
+ note = NL + NL + '(truncated at ' + AT_REF_FILE_CAP + ' chars)';
76
+ }
77
+ total += body.length;
78
+ inlined.push(path.relative(workdir, file) || file);
79
+ blocks.push('### ' + ref + NL + '```' + NL + body + NL + '```' + note);
80
+ }
81
+ if (!blocks.length)
82
+ return { text, inlined: [] };
83
+ const section = NL +
84
+ NL +
85
+ '## Files referenced with @ in the task' +
86
+ NL +
87
+ NL +
88
+ blocks.join(NL + NL);
89
+ return { text: text + section, inlined };
90
+ }
@@ -0,0 +1,80 @@
1
+ // Conventional-Commits -> CHANGELOG draft (BACKLOG D2). PURE and
2
+ // unit-tested: the draft is built from git log OUTSIDE the release flow
3
+ // (scripts/changelog-draft.mts), so a misparse never touches CHANGELOG.md.
4
+ // We do NOT auto-write the file: a human (or the agent) reviews the draft
5
+ // first -- the changelog must stay a deliberate document, not a byproduct.
6
+ // Parse one commit SUBJECT into a conventional commit, or null when it
7
+ // does not follow the format (a free-form subject is skipped from the draft).
8
+ // Supported forms:
9
+ // feat: add X feat(cli): add X
10
+ // fix!: change Y feat!: breaking
11
+ // Explicit breaking markers: `!` after the type/scope. A BREAKING CHANGE
12
+ // body footer is beyond a subject-only parser (git log %s only) -- out of scope.
13
+ export function parseConventionalCommit(subject) {
14
+ const s = String(subject ?? '').trim();
15
+ if (!s)
16
+ return null;
17
+ // <type>(<scope>)!?: <description>
18
+ const m = /^([a-zA-Z]+)(\(.+?\))?(!)?:\s*(.+)$/.exec(s);
19
+ if (!m)
20
+ return null;
21
+ const type = m[1].toLowerCase();
22
+ const scope = m[2] ? m[2].slice(1, -1).trim() : undefined;
23
+ const breaking = m[3] === '!';
24
+ const description = (m[4] || '').trim();
25
+ if (!description)
26
+ return null;
27
+ return { type, scope, breaking, description };
28
+ }
29
+ const KIND_BY_TYPE = {
30
+ feat: 'Added',
31
+ fix: 'Fixed',
32
+ perf: 'Changed',
33
+ refactor: 'Changed',
34
+ style: 'Changed',
35
+ revert: 'Changed',
36
+ remove: 'Removed',
37
+ };
38
+ // Types that never belong in a user-facing changelog (build/CI/internal).
39
+ const SKIP_TYPES = new Set(['chore', 'ci', 'test', 'build', 'docs', 'internal']);
40
+ // Group parsed commits by changelog kind. Each entry is a short
41
+ // imperative line; a scope is prefixed as `**scope:** ` and a breaking marker
42
+ // is appended as `(BREAKING)`. Order within a group is preserved (commit
43
+ // order), and duplicate lines are dropped so a rebase that repeats a subject
44
+ // does not flip the draft twice.
45
+ export function groupCommits(forCommit) {
46
+ const groups = {
47
+ Added: [],
48
+ Fixed: [],
49
+ Changed: [],
50
+ Removed: [],
51
+ };
52
+ const seen = new Set();
53
+ for (const c of forCommit || []) {
54
+ const kind = KIND_BY_TYPE[c.type];
55
+ if (!kind || SKIP_TYPES.has(c.type))
56
+ continue;
57
+ const prefix = c.scope ? '**' + c.scope + ':** ' : '';
58
+ const line = '- ' + prefix + c.description + (c.breaking ? ' (BREAKING)' : '');
59
+ if (seen.has(line))
60
+ continue;
61
+ seen.add(line);
62
+ groups[kind].push(line);
63
+ }
64
+ return groups;
65
+ }
66
+ // Render the groups as a changelog body (the parts under a version
67
+ // heading), with the standard Keep-a-Changelog order and empty groups
68
+ // omitted. Returns '' when there is nothing to report.
69
+ export function renderChangelogDraft(groups) {
70
+ const order = ['Added', 'Fixed', 'Changed', 'Removed'];
71
+ const out = [];
72
+ for (const kind of order) {
73
+ const items = groups[kind] || [];
74
+ if (!items.length)
75
+ continue;
76
+ out.push('## ' + kind, '');
77
+ out.push(...items, '');
78
+ }
79
+ return out.join('\n').trimEnd();
80
+ }
package/dist/index.js CHANGED
@@ -11,7 +11,7 @@ import { createTodoStore, getTodos, setTodos, resetTodos, renderTodos, } from '.
11
11
  import { runAgentLoop } from './agent-loop.js';
12
12
  import { createSpinner } from './spinner.js';
13
13
  import { LineEditor, expandPastes, pasteReplacement, formatCompactTokens, } from './input.js';
14
- import { parseImagePaste, extForMime, saveToTemp, guessMime, isImageName, formatSize, looksLikeFilePath, readClipboardImageDetailed, sniffMime, readWindowsClipboardFiles, hasClipboardTool, writeClipboardText, contentHash, } from './attachments.js';
14
+ import { parseImagePaste, extForMime, saveToTemp, guessMime, formatSize, readClipboardImageDetailed, sniffMime, readWindowsClipboardFiles, hasClipboardTool, writeClipboardText, contentHash, } from './attachments.js';
15
15
  import { loadConfig, DEFAULTS, CONFIG_PATHS, ZAMES_HOME, CONFIG_SCHEMA, getByPath, validateConfigValue, writeConfigValue, resetConfigValue, } from './config.js';
16
16
  import { runConfigMenu } from './config-menu.js';
17
17
  import { translate, normalizeLocale, localeDisplayName, isLocale, } from './i18n.js';
@@ -23,7 +23,7 @@ import { formatDiff, formatDiffStat, formatContextSources, diffGitArgs, parseTra
23
23
  import { performCompact } from './compact.js';
24
24
  import { Scheduler, parseInterval, formatInterval, formatJobLine, parseCron, } from './scheduler.js';
25
25
  import { renderMarkdown, setAnswerWidth } from './markdown.js';
26
- import { extractAtFileRefs } from './path-token.js';
26
+ import { resolveAttachPath, inlineAtRefs } from './attach-refs.js';
27
27
  import { closeWeb } from './web.js';
28
28
  import { assertCommandInsideRoot, runShell } from './shell.js';
29
29
  import { saveSession, loadLastSession, readSession, listSessions, sessionsDir, loadHistory, saveHistory, } from './sessions.js';
@@ -627,88 +627,6 @@ function buildSlashCommands() {
627
627
  // We put the agent's temporary files (one-off scripts, etc.) in
628
628
  // <project>/tmp — this folder is in .gitignore and is cleaned on every launch.
629
629
  const TMP_DIR = path.join(__dirname, '..', 'tmp');
630
- // Resolve a pasted string into a path to an existing file. Handles quoted
631
- // paths (drag&drop from some file managers adds quotes) and paths relative to
632
- // the working directory.
633
- async function resolveAttachPath(workdir, raw) {
634
- let s = String(raw || '').trim();
635
- if (!s)
636
- return null;
637
- if ((s.startsWith('"') && s.endsWith('"')) ||
638
- (s.startsWith("'") && s.endsWith("'"))) {
639
- s = s.slice(1, -1);
640
- }
641
- s = s.replace(/\\ /g, ' ');
642
- if (!looksLikeFilePath(s) && !isImageName(s))
643
- return null;
644
- const candidates = path.isAbsolute(s)
645
- ? [s]
646
- : [
647
- // Most common: relative to the project (working) directory.
648
- path.resolve(workdir, s),
649
- // The operator may paste a path relative to the terminal's cwd (which
650
- // can be the parent of the project) or copy it with the project name
651
- // included, e.g. "zames_pro/tmp/image.png" while workdir is already
652
- // .../zames_pro. Try both so the marker appears instead of raw text.
653
- path.resolve(process.cwd(), s),
654
- path.resolve(workdir, '..', s),
655
- ];
656
- for (const c of candidates) {
657
- const st = await fs.stat(c).catch(() => null);
658
- if (st && st.isFile())
659
- return c;
660
- }
661
- return null;
662
- }
663
- // Per-file and total caps for `@path` inlining (B5). A reference should SAVE
664
- // round-trips, not flood the context: a huge file is truncated with a note
665
- // telling the agent to read the rest itself.
666
- const AT_REF_FILE_CAP = 60 * 1024;
667
- const AT_REF_TOTAL_CAP = 200 * 1024;
668
- // Inline `@path` references found in a task text (B5). Returns the augmented
669
- // text plus the list of files actually inlined (for the operator notice).
670
- // Unknown/unreadable references are left in place untouched — they may be part
671
- // of the prose; we never fail the task over a missing file.
672
- async function inlineAtRefs(workdir, text) {
673
- const refs = extractAtFileRefs(text);
674
- if (!refs.length)
675
- return { text, inlined: [] };
676
- const blocks = [];
677
- const inlined = [];
678
- let total = 0;
679
- for (const ref of refs) {
680
- if (total >= AT_REF_TOTAL_CAP)
681
- break;
682
- const file = await resolveAttachPath(workdir, ref);
683
- if (!file)
684
- continue;
685
- let raw;
686
- try {
687
- raw = await fs.readFile(file, 'utf-8');
688
- }
689
- catch {
690
- continue;
691
- }
692
- let body = raw;
693
- let note = '';
694
- if (body.length > AT_REF_FILE_CAP) {
695
- body = body.slice(0, AT_REF_FILE_CAP);
696
- note = NL + NL + '(truncated at ' + AT_REF_FILE_CAP + ' chars)';
697
- }
698
- total += body.length;
699
- inlined.push(path.relative(workdir, file) || file);
700
- blocks.push('### ' + ref + NL + '```' + NL + body + NL + '```' + note);
701
- }
702
- if (!blocks.length)
703
- return { text, inlined: [] };
704
- const section = NL +
705
- NL +
706
- '## Files referenced with @ in the task' +
707
- NL +
708
- NL +
709
- blocks.join(NL + NL);
710
- return { text: text + section, inlined };
711
- }
712
630
  async function cleanTmpDir() {
713
631
  try {
714
632
  await fs.rm(TMP_DIR, { recursive: true, force: true });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zames_pro",
3
- "version": "2.63.1",
3
+ "version": "2.64.0",
4
4
  "description": "Terminal coding agent that drives chat.deepseek.com through Playwright: reads and edits files, runs commands, commits to git.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -21,6 +21,7 @@
21
21
  "clean": "node scripts/clean-dist.mjs",
22
22
  "build": "node scripts/clean-dist.mjs && tsc -p tsconfig.build.json",
23
23
  "coverage:gate": "node scripts/coverage-gate.mjs",
24
+ "changelog:draft": "tsx scripts/changelog-draft.mts",
24
25
  "typecheck": "tsc --noEmit",
25
26
  "lint": "node scripts/eslint-ts6.mjs",
26
27
  "lint:fix": "node scripts/eslint-ts6.mjs --fix",
@@ -0,0 +1,56 @@
1
+ import { spawnSync } from 'node:child_process'
2
+ import {
3
+ parseConventionalCommit,
4
+ groupCommits,
5
+ renderChangelogDraft,
6
+ } from '../src/changelog.js'
7
+
8
+ // Draft a CHANGELOG section from conventional commits since a ref (BACKLOG
9
+ // D2). PRINTS to stdout and never touches CHANGELOG.md -- the operator (or
10
+ // the agent) pastes the result in by hand, so a bad commit subject can never
11
+ // corrupt the changelog.
12
+ //
13
+ // Usage:
14
+ // npm run changelog:draft # since the latest tag, or an explicit ref
15
+ // npm run changelog:draft -- v2.63.0
16
+
17
+ function git(args: string[]): string {
18
+ const r = spawnSync('git', args, { encoding: 'utf-8' })
19
+ if (r.status !== 0) return ''
20
+ return (r.stdout || '').toString()
21
+ }
22
+
23
+ const argRef = process.argv.slice(2).find((a) => !a.startsWith('-'))
24
+
25
+ // Resolve the start ref: an explicit arg, otherwise the latest tag,
26
+ // otherwise the root commit.
27
+ const latestTag = git(['describe', '--tags', '--abbrev=0']).trim()
28
+ const ref =
29
+ argRef ||
30
+ latestTag ||
31
+ git(['revlist', 'HEAD']).trim().split('\n').pop() ||
32
+ 'HEAD'
33
+
34
+ const subjects = git([
35
+ 'log',
36
+ '--no-merges',
37
+ '--pretty=format:%s',
38
+ `${ref}..HEAD`,
39
+ ])
40
+ .split('\n')
41
+ .map((s) => s.trim())
42
+ .filter(Boolean)
43
+
44
+ const parsed = subjects
45
+ .map(parseConventionalCommit)
46
+ .filter((c): c is NonNullable<typeof c> => c !== null)
47
+ const body = renderChangelogDraft(groupCommits(parsed))
48
+
49
+ if (!body) {
50
+ console.error('no conventional commits since ' + ref)
51
+ process.exit(0)
52
+ }
53
+
54
+ console.log('## [Unreleased]')
55
+ console.log('')
56
+ console.log(body)
@@ -30,12 +30,18 @@ const FLOORS = {
30
30
  'undo.ts': 85,
31
31
  'checkpoint.ts': 80,
32
32
  'permissions.ts': 85,
33
+ 'changelog.ts': 85,
34
+ 'attach-refs.ts': 85,
33
35
  }
34
36
 
35
37
  const run = spawnSync(
36
38
  'npx',
37
39
  ['tsx', '--test', '--experimental-test-coverage', 'test/*.test.ts'],
38
- { encoding: 'utf-8', shell: process.platform === 'win32', maxBuffer: 1024 * 1024 * 64 },
40
+ {
41
+ encoding: 'utf-8',
42
+ shell: process.platform === 'win32',
43
+ maxBuffer: 1024 * 1024 * 64,
44
+ },
39
45
  )
40
46
 
41
47
  const out = (run.stdout || '') + (run.stderr || '')