@cspeach/cli 1.1.14 → 1.1.15
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/README.md +63 -0
- package/dist/cli.js +81 -27
- package/dist/commands/project.js +301 -0
- package/dist/commands/team.js +1166 -0
- package/dist/projects/save-command.js +20 -0
- package/dist/register/atc-file.js +802 -0
- package/dist/register/baseline.js +106 -0
- package/dist/register/measure.js +380 -0
- package/dist/register/records.js +159 -0
- package/dist/register/store.js +227 -0
- package/dist/repl.js +10 -2
- package/dist/rewind/candidates.js +14 -1
- package/dist/rewind/restore.js +53 -19
- package/dist/skill-catalog.js +33 -0
- package/dist/skills/bundled-skills.js +1 -1
- package/dist/tools/filesystem/extract-document.js +6 -0
- package/dist/tools/filesystem/extract-xlsx.js +139 -0
- package/dist/tools/filesystem/file-write.js +20 -0
- package/dist/tools/filesystem/read-document.js +15 -6
- package/dist/tools/include-snapshot.js +24 -0
- package/dist/tools/sap-write.js +19 -9
- package/dist/tools/snapshot.js +27 -0
- package/package.json +9 -6
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import { extractText, getDocumentProxy } from 'unpdf';
|
|
4
4
|
import mammoth from 'mammoth';
|
|
5
5
|
import TurndownService from 'turndown';
|
|
6
|
+
import { extractXlsx } from './extract-xlsx.js';
|
|
6
7
|
/** Thrown when a PDF exceeds the caller's page ceiling — checked before full extraction. */
|
|
7
8
|
export class DocumentTooLargeError extends Error {
|
|
8
9
|
pages;
|
|
@@ -20,6 +21,11 @@ export async function extractDocument(buffer, ext, opts) {
|
|
|
20
21
|
return extractPdf(buffer, opts?.maxPages);
|
|
21
22
|
if (ext === '.docx')
|
|
22
23
|
return extractDocx(buffer);
|
|
24
|
+
// 2026-09-23 — specs and object lists arrive as spreadsheets (Glenn).
|
|
25
|
+
if (ext === '.xlsx') {
|
|
26
|
+
const x = extractXlsx(buffer);
|
|
27
|
+
return { text: x.text, sheets: x.sheets, warnings: x.warnings };
|
|
28
|
+
}
|
|
23
29
|
throw new Error(`extractDocument: unsupported extension "${ext}"`);
|
|
24
30
|
}
|
|
25
31
|
async function extractPdf(buffer, maxPages) {
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* .xlsx text extraction (2026-09-23 — Glenn: "it cannot read the excel file").
|
|
3
|
+
*
|
|
4
|
+
* Specs and object lists arrive as spreadsheets, so read_document reads them
|
|
5
|
+
* like any other document. An .xlsx IS a zip of XML parts, and adm-zip already
|
|
6
|
+
* ships inside the tree (SAP's own @sap-ux packages pull it), so nothing new
|
|
7
|
+
* enters the product to do this.
|
|
8
|
+
*
|
|
9
|
+
* Output shape: one "## Sheet: <name>" heading per sheet, then one line per
|
|
10
|
+
* row, cells separated by tabs, empty trailing cells dropped and empty leading
|
|
11
|
+
* ones kept so columns still line up. A model reads that as a table; a human
|
|
12
|
+
* reading the transcript can too.
|
|
13
|
+
*
|
|
14
|
+
* Deliberately NOT done: formulas (the cached VALUE is used, which is what the
|
|
15
|
+
* sheet shows), formatting, merged-cell geometry, charts, images. Dates come
|
|
16
|
+
* out as the serial number Excel stores unless the cell is text — the caller is
|
|
17
|
+
* told, rather than us guessing a locale.
|
|
18
|
+
*/
|
|
19
|
+
import AdmZip from 'adm-zip';
|
|
20
|
+
/** Hard ceilings so one huge workbook cannot fill a turn. */
|
|
21
|
+
export const MAX_SHEET_ROWS = 2000;
|
|
22
|
+
export const MAX_CELLS = 100_000;
|
|
23
|
+
const decode = (s) => s.replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"')
|
|
24
|
+
.replace(/'/g, "'").replace(/&#x([0-9a-fA-F]+);/g, (_, h) => String.fromCodePoint(parseInt(h, 16)))
|
|
25
|
+
.replace(/&#(\d+);/g, (_, d) => String.fromCodePoint(Number(d)))
|
|
26
|
+
.replace(/&/g, '&'); // last: an encoded "&lt;" must not become "<"
|
|
27
|
+
/** All <t> text inside one shared-string <si>, joined (rich text is split across runs). */
|
|
28
|
+
function siText(si) {
|
|
29
|
+
const parts = [...si.matchAll(/<t[^>]*>([\s\S]*?)<\/t>/g)].map((m) => decode(m[1]));
|
|
30
|
+
return parts.join('');
|
|
31
|
+
}
|
|
32
|
+
export function sharedStrings(xml) {
|
|
33
|
+
if (!xml)
|
|
34
|
+
return [];
|
|
35
|
+
return [...xml.matchAll(/<si>([\s\S]*?)<\/si>/g)].map((m) => siText(m[1]));
|
|
36
|
+
}
|
|
37
|
+
/** Column letters → 0-based index: A→0, B→1, AA→26. */
|
|
38
|
+
export function colIndex(ref) {
|
|
39
|
+
const letters = /^([A-Z]+)/.exec(ref.toUpperCase())?.[1] ?? 'A';
|
|
40
|
+
let n = 0;
|
|
41
|
+
for (const ch of letters)
|
|
42
|
+
n = n * 26 + (ch.charCodeAt(0) - 64);
|
|
43
|
+
return n - 1;
|
|
44
|
+
}
|
|
45
|
+
/** One worksheet's cells as a rectangular grid of strings. */
|
|
46
|
+
export function sheetRows(xml, strings, budget) {
|
|
47
|
+
const rows = [];
|
|
48
|
+
for (const rowMatch of xml.matchAll(/<row[^>]*>([\s\S]*?)<\/row>/g)) {
|
|
49
|
+
if (rows.length >= MAX_SHEET_ROWS || budget.cells <= 0)
|
|
50
|
+
break;
|
|
51
|
+
const cells = [];
|
|
52
|
+
for (const cell of rowMatch[1].matchAll(/<c\b([^>]*)>([\s\S]*?)<\/c>|<c\b([^>]*)\/>/g)) {
|
|
53
|
+
const attrs = cell[1] ?? cell[3] ?? '';
|
|
54
|
+
const body = cell[2] ?? '';
|
|
55
|
+
const at = colIndex(/r="([A-Z]+\d+)"/.exec(attrs)?.[1] ?? 'A1');
|
|
56
|
+
const type = /t="([^"]+)"/.exec(attrs)?.[1];
|
|
57
|
+
let value = '';
|
|
58
|
+
if (type === 's') {
|
|
59
|
+
const idx = Number(/<v>([\s\S]*?)<\/v>/.exec(body)?.[1] ?? '-1');
|
|
60
|
+
value = strings[idx] ?? '';
|
|
61
|
+
}
|
|
62
|
+
else if (type === 'inlineStr') {
|
|
63
|
+
value = siText(body);
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
// n (number), str (formula result), b, d, e — the cached value as shown.
|
|
67
|
+
value = decode(/<v>([\s\S]*?)<\/v>/.exec(body)?.[1] ?? '');
|
|
68
|
+
}
|
|
69
|
+
while (cells.length < at)
|
|
70
|
+
cells.push('');
|
|
71
|
+
cells[at] = value.replace(/[\t\r\n]+/g, ' ').trim();
|
|
72
|
+
budget.cells -= 1;
|
|
73
|
+
if (budget.cells <= 0)
|
|
74
|
+
break;
|
|
75
|
+
}
|
|
76
|
+
while (cells.length > 0 && cells[cells.length - 1] === '')
|
|
77
|
+
cells.pop();
|
|
78
|
+
rows.push(cells);
|
|
79
|
+
}
|
|
80
|
+
while (rows.length > 0 && rows[rows.length - 1].length === 0)
|
|
81
|
+
rows.pop();
|
|
82
|
+
return rows;
|
|
83
|
+
}
|
|
84
|
+
/** Sheet name → part path, in the workbook's own order. */
|
|
85
|
+
export function sheetOrder(workbookXml, relsXml) {
|
|
86
|
+
if (!workbookXml)
|
|
87
|
+
return [];
|
|
88
|
+
const rels = new Map();
|
|
89
|
+
for (const m of (relsXml ?? '').matchAll(/<Relationship\b([^>]*)\/>/g)) {
|
|
90
|
+
const id = /Id="([^"]+)"/.exec(m[1])?.[1];
|
|
91
|
+
const target = /Target="([^"]+)"/.exec(m[1])?.[1];
|
|
92
|
+
if (id && target)
|
|
93
|
+
rels.set(id, target.replace(/^\/?xl\//, '').replace(/^\//, ''));
|
|
94
|
+
}
|
|
95
|
+
const out = [];
|
|
96
|
+
for (const m of workbookXml.matchAll(/<sheet\b([^>]*)\/>/g)) {
|
|
97
|
+
const attrs = m[1];
|
|
98
|
+
const name = decode(/name="([^"]*)"/.exec(attrs)?.[1] ?? `Sheet${out.length + 1}`);
|
|
99
|
+
const rid = /r:id="([^"]+)"/.exec(attrs)?.[1] ?? '';
|
|
100
|
+
out.push({ name, path: `xl/${rels.get(rid) ?? `worksheets/sheet${out.length + 1}.xml`}` });
|
|
101
|
+
}
|
|
102
|
+
return out;
|
|
103
|
+
}
|
|
104
|
+
export function extractXlsx(buffer) {
|
|
105
|
+
let zip;
|
|
106
|
+
try {
|
|
107
|
+
zip = new AdmZip(buffer);
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
return { text: '', sheets: 0, warnings: ['not a readable .xlsx (the file may be .xls, password-protected or damaged)'] };
|
|
111
|
+
}
|
|
112
|
+
const read = (p) => {
|
|
113
|
+
const entry = zip.getEntry(p);
|
|
114
|
+
return entry ? entry.getData().toString('utf-8') : null;
|
|
115
|
+
};
|
|
116
|
+
const strings = sharedStrings(read('xl/sharedStrings.xml'));
|
|
117
|
+
const sheets = sheetOrder(read('xl/workbook.xml'), read('xl/_rels/workbook.xml.rels'));
|
|
118
|
+
if (sheets.length === 0) {
|
|
119
|
+
return { text: '', sheets: 0, warnings: ['no sheets found — the file may be .xls (old format); save it as .xlsx'] };
|
|
120
|
+
}
|
|
121
|
+
const budget = { cells: MAX_CELLS };
|
|
122
|
+
const warnings = [];
|
|
123
|
+
const blocks = [];
|
|
124
|
+
for (const s of sheets) {
|
|
125
|
+
const xml = read(s.path);
|
|
126
|
+
if (xml === null)
|
|
127
|
+
continue;
|
|
128
|
+
const rows = sheetRows(xml, strings, budget);
|
|
129
|
+
if (rows.length >= MAX_SHEET_ROWS)
|
|
130
|
+
warnings.push(`sheet "${s.name}" was cut at ${MAX_SHEET_ROWS} rows`);
|
|
131
|
+
blocks.push(`## Sheet: ${s.name}\n\n${rows.map((r) => r.join('\t')).join('\n')}`.trimEnd());
|
|
132
|
+
}
|
|
133
|
+
if (budget.cells <= 0)
|
|
134
|
+
warnings.push('the workbook was cut short — it holds more cells than one read returns');
|
|
135
|
+
const text = blocks.join('\n\n').trim();
|
|
136
|
+
if (text.length === 0)
|
|
137
|
+
warnings.push('no cell text found — the workbook may be empty or hold only charts/images');
|
|
138
|
+
return { text, sheets: sheets.length, warnings };
|
|
139
|
+
}
|
|
@@ -21,6 +21,25 @@ import { promises as fs } from 'node:fs';
|
|
|
21
21
|
import * as path from 'node:path';
|
|
22
22
|
import { registerTool } from '../index.js';
|
|
23
23
|
import { resolveSafePath, assertRealPathContained, PathOutsideRootError, DENYLIST_DESCRIPTION, isDenylistedPath, blockedExecutableExtension, } from '../_filesystem-shared.js';
|
|
24
|
+
/**
|
|
25
|
+
* 2026-09-23 (Glenn's first run) — a standalone run writes GUIDE.md and said
|
|
26
|
+
* nothing about it. He opened it in a text editor, then tried to drop it into
|
|
27
|
+
* an unrelated page on the site and got an error: "what does this file do …
|
|
28
|
+
* where would this enter back into the CSPEACH process?". Say it once, when
|
|
29
|
+
* the file appears, in the human stream.
|
|
30
|
+
*/
|
|
31
|
+
let guideNoteShown = false;
|
|
32
|
+
export function __resetGuideNoteForTests() { guideNoteShown = false; }
|
|
33
|
+
function noteGuideWritten(relPath, ctx) {
|
|
34
|
+
if (guideNoteShown)
|
|
35
|
+
return;
|
|
36
|
+
if (path.basename(relPath).toLowerCase() !== 'guide.md')
|
|
37
|
+
return;
|
|
38
|
+
guideNoteShown = true;
|
|
39
|
+
ctx.chunkEmitter?.emit('info', `${relPath} is your step-by-step list for applying these objects in SAP — `
|
|
40
|
+
+ 'open it at https://cspeach.dev/guide, or in any text editor. '
|
|
41
|
+
+ 'If a step fails, come back here and say "step N failed" with the error.');
|
|
42
|
+
}
|
|
24
43
|
export async function fileWriteHandler(args, ctx) {
|
|
25
44
|
// 1. Path containment — sync phase (pure string math, catches .. traversals)
|
|
26
45
|
let abs;
|
|
@@ -110,6 +129,7 @@ export async function fileWriteHandler(args, ctx) {
|
|
|
110
129
|
}
|
|
111
130
|
const relPath = path.relative(path.resolve(ctx.cwd), target).replace(/\\/g, '/');
|
|
112
131
|
const bytes = Buffer.byteLength(args.content, 'utf-8');
|
|
132
|
+
noteGuideWritten(relPath, ctx);
|
|
113
133
|
return { content: `wrote ${relPath} (${bytes} bytes)` };
|
|
114
134
|
}
|
|
115
135
|
registerTool({
|
|
@@ -12,7 +12,7 @@ import { extractDocument, DocumentTooLargeError } from './extract-document.js';
|
|
|
12
12
|
const MAX_LINES = 2000;
|
|
13
13
|
const MAX_BYTES = 50 * 1024 * 1024; // 50 MB
|
|
14
14
|
const MAX_PAGES = 150;
|
|
15
|
-
const SUPPORTED = new Set(['.pdf', '.docx']);
|
|
15
|
+
const SUPPORTED = new Set(['.pdf', '.docx', '.xlsx']);
|
|
16
16
|
export async function readDocumentHandler(args, ctx) {
|
|
17
17
|
if (args.offset !== undefined && args.offset < 0) {
|
|
18
18
|
return { content: `error: offset must be non-negative (got ${args.offset})`, is_error: true };
|
|
@@ -32,7 +32,10 @@ export async function readDocumentHandler(args, ctx) {
|
|
|
32
32
|
const ext = path.extname(abs).toLowerCase();
|
|
33
33
|
if (!SUPPORTED.has(ext)) {
|
|
34
34
|
return {
|
|
35
|
-
content:
|
|
35
|
+
content: ext === '.xls' || ext === '.csv'
|
|
36
|
+
? `error: read_document reads .xlsx, not ${ext}; save "${args.path}" as .xlsx`
|
|
37
|
+
+ (ext === '.csv' ? ' — or read a .csv with file_read, it is plain text' : '')
|
|
38
|
+
: `error: read_document supports .pdf, .docx and .xlsx; convert "${args.path}" first (.doc/.pptx/scanned images are not supported)`,
|
|
36
39
|
is_error: true,
|
|
37
40
|
};
|
|
38
41
|
}
|
|
@@ -74,8 +77,14 @@ export async function readDocumentHandler(args, ctx) {
|
|
|
74
77
|
}
|
|
75
78
|
return { content: `error: could not read "${args.path}": ${err instanceof Error ? err.message : String(err)}`, is_error: true };
|
|
76
79
|
}
|
|
77
|
-
// 'document' is the intentional label for .docx: result.pages is PDF-only
|
|
78
|
-
|
|
80
|
+
// 'document' is the intentional label for .docx: result.pages is PDF-only,
|
|
81
|
+
// result.sheets is .xlsx-only, and a Word file has neither.
|
|
82
|
+
const scale = result.pages !== undefined
|
|
83
|
+
? `${result.pages} pages`
|
|
84
|
+
: result.sheets !== undefined
|
|
85
|
+
? `${result.sheets} sheet${result.sheets === 1 ? '' : 's'}`
|
|
86
|
+
: 'document';
|
|
87
|
+
const header = `${args.path} — ${scale}`;
|
|
79
88
|
const warnLines = result.warnings.map((w) => `[warning] ${w}`);
|
|
80
89
|
const allLines = result.text.replace(/\n$/, '').split('\n');
|
|
81
90
|
const offset = Math.max(0, args.offset ?? 0);
|
|
@@ -90,14 +99,14 @@ export async function readDocumentHandler(args, ctx) {
|
|
|
90
99
|
}
|
|
91
100
|
registerTool({
|
|
92
101
|
name: 'read_document',
|
|
93
|
-
description: 'Read a .pdf or .
|
|
102
|
+
description: 'Read a .pdf, .docx or .xlsx and return its text with page/sheet/heading markers and line numbers. A spreadsheet comes back one sheet at a time, rows as tab-separated cells (cached values, not formulas). Optional offset + limit for windowed reads of large documents.',
|
|
94
103
|
isMutating: false,
|
|
95
104
|
category: 'filesystem',
|
|
96
105
|
flagGated: true,
|
|
97
106
|
input_schema: {
|
|
98
107
|
type: 'object',
|
|
99
108
|
properties: {
|
|
100
|
-
path: { type: 'string', description: 'Path to a .pdf or .
|
|
109
|
+
path: { type: 'string', description: 'Path to a .pdf, .docx or .xlsx, relative to the project root or absolute inside it.' },
|
|
101
110
|
offset: { type: 'number', description: 'Optional 0-based line offset into the extracted text.' },
|
|
102
111
|
limit: { type: 'number', description: 'Optional max lines to return (cap 2000).' },
|
|
103
112
|
},
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Snapshot-store key for a class LOCAL INCLUDE (CCDEF / CCIMP / CCMAC / test
|
|
3
|
+
* classes). Pure — no fs, no SAP.
|
|
4
|
+
*
|
|
5
|
+
* Why a separate key (2026-09-21, found on the first clean-core fix): the
|
|
6
|
+
* include write used to snapshot the class MAIN source. For a RAP behaviour
|
|
7
|
+
* pool the main source is an 8-line shell and the change lives in CCIMP — so
|
|
8
|
+
* the Rule-7 backup never covered the change, and rewind wrote the unchanged
|
|
9
|
+
* main source back and reported success while the include change stayed.
|
|
10
|
+
*
|
|
11
|
+
* The include is stored under type `CLAS.<INCLUDE>` (e.g. `CLAS.LOCALS_IMP`),
|
|
12
|
+
* name = class name. A dot keeps the store's two-level type/name layout (no
|
|
13
|
+
* nested dirs) and can never collide with a real ABAP type code.
|
|
14
|
+
*/
|
|
15
|
+
const INCLUDE_TYPE_RE = /^CLAS\.(LOCALS_DEF|LOCALS_IMP|MACROS|TESTCLASSES)$/i;
|
|
16
|
+
/** Snapshot type key for one include of a class. */
|
|
17
|
+
export function includeSnapshotType(includeType) {
|
|
18
|
+
return `CLAS.${includeType.toUpperCase()}`;
|
|
19
|
+
}
|
|
20
|
+
/** The include a snapshot type key names, or null for an ordinary object type. */
|
|
21
|
+
export function parseIncludeSnapshotType(type) {
|
|
22
|
+
const m = INCLUDE_TYPE_RE.exec(type);
|
|
23
|
+
return m ? m[1].toLowerCase() : null;
|
|
24
|
+
}
|
package/dist/tools/sap-write.js
CHANGED
|
@@ -24,7 +24,8 @@
|
|
|
24
24
|
import crypto from 'node:crypto';
|
|
25
25
|
import { registerTool } from './index.js';
|
|
26
26
|
import { verifyAndSpendApprovalId } from '../approvals/jwt.js';
|
|
27
|
-
import { autoSnapshot } from './snapshot.js';
|
|
27
|
+
import { autoSnapshot, autoSnapshotClassInclude } from './snapshot.js';
|
|
28
|
+
import { includeSnapshotType } from './include-snapshot.js';
|
|
28
29
|
import { buildWriteChainLine } from '../renderer/verify-chain.js';
|
|
29
30
|
import { verifySyntax, verifyActive } from './verify.js';
|
|
30
31
|
import { appendPending, finalizeToolCall } from '../session/pending.js';
|
|
@@ -663,9 +664,9 @@ registerTool({
|
|
|
663
664
|
// Writes a class local-include source (CCDEF/CCIMP/CCMAC/test classes). This is
|
|
664
665
|
// how RAP behaviour-pool handlers (which live in CCIMP / locals_imp) get landed.
|
|
665
666
|
// AdtClient locks the CLASS object, tries a ladder of PUT URL/content-type
|
|
666
|
-
// variants (first 2xx wins), and always unlocks.
|
|
667
|
-
//
|
|
668
|
-
//
|
|
667
|
+
// variants (first 2xx wins), and always unlocks. The Rule-7 snapshot is of the
|
|
668
|
+
// INCLUDE itself (autoSnapshotClassInclude). It is non-blocking — a fresh
|
|
669
|
+
// class's include may not be readable yet — but a failed one warns loudly.
|
|
669
670
|
registerTool({
|
|
670
671
|
name: 'sap_set_class_include',
|
|
671
672
|
description: 'Write a class local-include source (CCDEF locals_def, CCIMP locals_imp, '
|
|
@@ -713,15 +714,24 @@ registerTool({
|
|
|
713
714
|
// The CCIMP piece (LIMU CINC) is exactly the kind of object that sits
|
|
714
715
|
// locked in a request from earlier work — battery S1's 409 root cause.
|
|
715
716
|
const resolved = await resolveWriteTransport(ctx, 'CLAS', args.className, effectiveTransport);
|
|
716
|
-
// ── Gate 2: Snapshot (Rule 7) —
|
|
717
|
+
// ── Gate 2: Snapshot (Rule 7) — of the INCLUDE being overwritten ─────────
|
|
718
|
+
// Not the class main source: for a behaviour pool that is an empty shell and
|
|
719
|
+
// the change lives in CCIMP (2026-09-21 — the old main-source snapshot left
|
|
720
|
+
// the change with no restore point, and rewind "restored" the wrong thing).
|
|
721
|
+
// Still non-blocking (a fresh class's include may not be readable yet), but
|
|
722
|
+
// a missing restore point is now said out loud instead of swallowed.
|
|
717
723
|
let snapOutcome;
|
|
718
724
|
try {
|
|
719
|
-
snapOutcome = await
|
|
725
|
+
snapOutcome = await autoSnapshotClassInclude(args.className, args.includeType, ctx.adt);
|
|
720
726
|
}
|
|
721
|
-
catch {
|
|
722
|
-
|
|
727
|
+
catch (err) {
|
|
728
|
+
snapOutcome = { taken: false, reason: 'source_read_failed', detail: String(err) };
|
|
729
|
+
}
|
|
730
|
+
emitSnapshotNotice(ctx, snapOutcome, includeSnapshotType(args.includeType), args.className);
|
|
731
|
+
if (snapOutcome?.reason === 'source_read_failed') {
|
|
732
|
+
ctx.chunkEmitter?.emit('warn', `CLAS ${args.className} ${args.includeType}: could not snapshot the include before writing `
|
|
733
|
+
+ `(${snapOutcome.detail ?? 'read failed'}) — this write has NO restore point; rewind cannot undo it`);
|
|
723
734
|
}
|
|
724
|
-
emitSnapshotNotice(ctx, snapOutcome, 'CLAS', args.className);
|
|
725
735
|
// ── WAL: record pending entry ────────────────────────────────────────────
|
|
726
736
|
await appendPending(ctx.session, {
|
|
727
737
|
tool_use_id: toolUseId,
|
package/dist/tools/snapshot.js
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
*/
|
|
16
16
|
import { registerTool } from './index.js';
|
|
17
17
|
import { snapshots, SapError } from '@cspeach/sap-client';
|
|
18
|
+
import { includeSnapshotType } from './include-snapshot.js';
|
|
18
19
|
/** True only for a genuine ADT 404 — the object does not exist. Any other
|
|
19
20
|
* failure (network, auth, 5xx, timeout) is a READ failure, not proof of
|
|
20
21
|
* absence, and must never be labelled "object not found". */
|
|
@@ -100,6 +101,32 @@ export async function autoSnapshot(objectName, objectType, adt) {
|
|
|
100
101
|
const entry = await snapshots.take(objectType, objectName, src, 'before_write');
|
|
101
102
|
return { taken: true, entry };
|
|
102
103
|
}
|
|
104
|
+
/**
|
|
105
|
+
* Rule-7 snapshot of ONE class local include, taken before sap_set_class_include
|
|
106
|
+
* overwrites it. Stored under includeSnapshotType(includeType) + class name so
|
|
107
|
+
* rewind can put the include (not the class main source) back.
|
|
108
|
+
*
|
|
109
|
+
* 404 → 'object_not_found' (include has no source yet — nothing to protect)
|
|
110
|
+
* other read error → 'source_read_failed' (+ detail) — caller decides; no restore point
|
|
111
|
+
* read OK (even '') → snapshot taken
|
|
112
|
+
* A snapshots.take failure THROWS, like autoSnapshot.
|
|
113
|
+
*/
|
|
114
|
+
export async function autoSnapshotClassInclude(className, includeType, adt) {
|
|
115
|
+
let src;
|
|
116
|
+
try {
|
|
117
|
+
src = await adt.classIncludes(className, includeType);
|
|
118
|
+
}
|
|
119
|
+
catch (err) {
|
|
120
|
+
if (isNotFound(err))
|
|
121
|
+
return { taken: false, reason: 'object_not_found' };
|
|
122
|
+
return { taken: false, reason: 'source_read_failed', detail: shortErrorMessage(err) };
|
|
123
|
+
}
|
|
124
|
+
if (typeof src !== 'string') {
|
|
125
|
+
return { taken: false, reason: 'source_read_failed', detail: 'include read returned no source text' };
|
|
126
|
+
}
|
|
127
|
+
const entry = await snapshots.take(includeSnapshotType(includeType), className, src, 'before_write');
|
|
128
|
+
return { taken: true, entry };
|
|
129
|
+
}
|
|
103
130
|
// ── sap_snapshot_list ─────────────────────────────────────────────────────────
|
|
104
131
|
registerTool({
|
|
105
132
|
name: 'sap_snapshot_list',
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cspeach/cli",
|
|
3
|
-
"version": "1.1.
|
|
4
|
-
"description": "CSPeach
|
|
3
|
+
"version": "1.1.15",
|
|
4
|
+
"description": "CSPeach \u2014 AI-assisted ABAP development CLI. Direct SAP access, 34 skills, safety gates.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"abap",
|
|
7
7
|
"sap",
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
"prebuild": "tsx scripts/prebuild.ts",
|
|
33
33
|
"build": "tsc && node scripts/make-bin-executable.js",
|
|
34
34
|
"install:global": "tsx scripts/install-global.ts",
|
|
35
|
-
"pretest": "tsx scripts/sync-signing-key.ts || node -e \"console.warn('WARN: sync-signing-key skipped
|
|
35
|
+
"pretest": "tsx scripts/sync-signing-key.ts || node -e \"console.warn('WARN: sync-signing-key skipped \u2014 using stub')\"",
|
|
36
36
|
"start": "node dist/index.js",
|
|
37
37
|
"typecheck": "tsc --noEmit",
|
|
38
38
|
"test": "vitest run",
|
|
@@ -44,22 +44,25 @@
|
|
|
44
44
|
"bench:audit": "tsx scripts/bench-audit-task.ts",
|
|
45
45
|
"fiori": "tsx src/tools/fiori/bin.ts",
|
|
46
46
|
"prepare": "husky",
|
|
47
|
-
"prepublishOnly": "node -e \"const
|
|
47
|
+
"prepublishOnly": "node -e \"const deps=require('./package.json').dependencies||{}; for (const n of ['@cspeach/sap-client','@cspeach/register-core']) { const d=deps[n]||''; if(d.startsWith('file:')||d.startsWith('link:')){console.error('publish blocked: '+n+' is a local link ('+d+') \u2014 point it at the published version before npm publish'); process.exit(1)} }\""
|
|
48
48
|
},
|
|
49
49
|
"engines": {
|
|
50
50
|
"node": ">=20.0.0"
|
|
51
51
|
},
|
|
52
52
|
"dependencies": {
|
|
53
53
|
"@anthropic-ai/sdk": "^0.90.0",
|
|
54
|
-
"@cspeach/
|
|
54
|
+
"@cspeach/register-core": "0.1.0",
|
|
55
|
+
"@cspeach/sap-client": "0.1.12",
|
|
55
56
|
"@iarna/toml": "^2.2.5",
|
|
56
57
|
"@inkjs/ui": "^2.0.0",
|
|
57
58
|
"@inquirer/prompts": "^7.0.0",
|
|
59
|
+
"@napi-rs/keyring": "2.1.0",
|
|
58
60
|
"@noble/ed25519": "2.1.0",
|
|
59
61
|
"@noble/hashes": "1.4.0",
|
|
60
62
|
"@sap-ux/fe-fpm-writer": "0.43.7",
|
|
61
63
|
"@sap-ux/fiori-elements-writer": "^3.0.39",
|
|
62
64
|
"@sap-ux/fiori-freestyle-writer": "3.0.12",
|
|
65
|
+
"adm-zip": "0.5.17",
|
|
63
66
|
"chalk": "^5.3.0",
|
|
64
67
|
"cli-highlight": "^2.1.11",
|
|
65
68
|
"cli-table3": "^0.6.5",
|
|
@@ -71,7 +74,6 @@
|
|
|
71
74
|
"ink": "^5.0.0",
|
|
72
75
|
"ink-text-input": "^6.0.0",
|
|
73
76
|
"jose": "^5.9.0",
|
|
74
|
-
"@napi-rs/keyring": "2.1.0",
|
|
75
77
|
"mammoth": "^1.8.0",
|
|
76
78
|
"marked": "^11.2.0",
|
|
77
79
|
"marked-terminal": "^6.2.0",
|
|
@@ -86,6 +88,7 @@
|
|
|
86
88
|
"zod": "^3.23.0"
|
|
87
89
|
},
|
|
88
90
|
"devDependencies": {
|
|
91
|
+
"@types/adm-zip": "0.5.8",
|
|
89
92
|
"@types/cross-spawn": "^6.0.6",
|
|
90
93
|
"@types/diff": "^7.0.2",
|
|
91
94
|
"@types/ejs": "^3.1.5",
|