@cspeach/cli 1.1.14 → 1.1.16
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/agent/loop.js +32 -7
- package/dist/agent/tool-dispatch.js +13 -9
- package/dist/cli.js +81 -27
- package/dist/commands/compact.js +6 -2
- package/dist/commands/project-context-impact.js +2 -2
- package/dist/commands/project.js +301 -0
- package/dist/commands/team.js +1166 -0
- package/dist/one-shot.js +2 -0
- package/dist/projects/image-attachments.js +155 -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/post-turn-status.js +2 -1
- package/dist/repl.js +16 -2
- package/dist/rewind/candidates.js +14 -1
- package/dist/rewind/restore.js +53 -19
- package/dist/router/classifier.js +24 -4
- package/dist/session/recap.js +8 -5
- package/dist/session/resume.js +37 -17
- package/dist/session/store.js +5 -3
- package/dist/session/user-prompt.js +23 -0
- 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/dist/ui/clipboard-image.js +125 -0
- package/dist/ui/footer.js +11 -1
- package/dist/ui/text-input.js +30 -2
- package/package.json +7 -4
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { promises as fs } from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { cspeachRoot } from '../config/paths.js';
|
|
5
|
+
let pasteDir = null;
|
|
6
|
+
let nextNumber = 1;
|
|
7
|
+
const pasted = new Map();
|
|
8
|
+
function dir() {
|
|
9
|
+
if (!pasteDir)
|
|
10
|
+
pasteDir = path.join(cspeachRoot(), 'pasted', `${Date.now()}-${process.pid}`);
|
|
11
|
+
return pasteDir;
|
|
12
|
+
}
|
|
13
|
+
/** Test seam: point the paste folder at a temp dir and reset numbering. */
|
|
14
|
+
export function __resetPastedImagesForTests(testDir) {
|
|
15
|
+
pasteDir = testDir;
|
|
16
|
+
nextNumber = 1;
|
|
17
|
+
pasted.clear();
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Save the clipboard image and return its chip, or null when the clipboard
|
|
21
|
+
* holds no image (or the platform tool is missing / fails). Never throws —
|
|
22
|
+
* a paste key must never break the prompt.
|
|
23
|
+
*/
|
|
24
|
+
export async function pasteClipboardImage(reader = readClipboardImage) {
|
|
25
|
+
const n = nextNumber;
|
|
26
|
+
const out = path.join(dir(), `image-${n}.png`);
|
|
27
|
+
try {
|
|
28
|
+
await fs.mkdir(dir(), { recursive: true });
|
|
29
|
+
if (!(await reader(out)))
|
|
30
|
+
return null;
|
|
31
|
+
const st = await fs.stat(out);
|
|
32
|
+
if (st.size === 0)
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
nextNumber = n + 1;
|
|
39
|
+
pasted.set(n, out);
|
|
40
|
+
return `[Image #${n}]`;
|
|
41
|
+
}
|
|
42
|
+
/** Swap each known `[Image #N]` chip for its quoted file path. */
|
|
43
|
+
export function expandImageChips(text) {
|
|
44
|
+
if (pasted.size === 0)
|
|
45
|
+
return text;
|
|
46
|
+
return text.replace(/\[Image #(\d+)\]/g, (chip, num) => {
|
|
47
|
+
const file = pasted.get(Number(num));
|
|
48
|
+
return file ? `"${file}"` : chip;
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* The platform command that writes the clipboard image to `outPath`.
|
|
53
|
+
* Windows and macOS write the file themselves and print "ok"; Linux tools
|
|
54
|
+
* print the PNG bytes to stdout (`stdoutToFile`). null = unsupported.
|
|
55
|
+
*/
|
|
56
|
+
export function clipboardImageCommand(platform, outPath, env) {
|
|
57
|
+
if (platform === 'win32') {
|
|
58
|
+
const p = outPath.replace(/'/g, "''");
|
|
59
|
+
const script = 'Add-Type -AssemblyName System.Windows.Forms; Add-Type -AssemblyName System.Drawing; ' +
|
|
60
|
+
'$i = [System.Windows.Forms.Clipboard]::GetImage(); ' +
|
|
61
|
+
`if ($i -ne $null) { $i.Save('${p}', [System.Drawing.Imaging.ImageFormat]::Png); 'ok' }`;
|
|
62
|
+
return { cmd: 'powershell.exe', args: ['-NoProfile', '-NonInteractive', '-STA', '-Command', script] };
|
|
63
|
+
}
|
|
64
|
+
if (platform === 'darwin') {
|
|
65
|
+
const p = outPath.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
|
66
|
+
return {
|
|
67
|
+
cmd: 'osascript',
|
|
68
|
+
args: [
|
|
69
|
+
'-e', 'try',
|
|
70
|
+
'-e', 'set d to the clipboard as «class PNGf»',
|
|
71
|
+
'-e', `set f to open for access POSIX file "${p}" with write permission`,
|
|
72
|
+
'-e', 'write d to f',
|
|
73
|
+
'-e', 'close access f',
|
|
74
|
+
'-e', 'return "ok"',
|
|
75
|
+
'-e', 'on error',
|
|
76
|
+
'-e', 'return "none"',
|
|
77
|
+
'-e', 'end try',
|
|
78
|
+
],
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
if (platform === 'linux') {
|
|
82
|
+
return env.WAYLAND_DISPLAY
|
|
83
|
+
? { cmd: 'wl-paste', args: ['--no-newline', '--type', 'image/png'], stdoutToFile: true }
|
|
84
|
+
: { cmd: 'xclip', args: ['-selection', 'clipboard', '-t', 'image/png', '-o'], stdoutToFile: true };
|
|
85
|
+
}
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Default reader: runs the platform command. stdin is CLOSED — Windows
|
|
90
|
+
* PowerShell 5.1 waits on an open stdin pipe and never exits (seen live
|
|
91
|
+
* 2026-09-23). 10 s ceiling covers a cold PowerShell start (~3 s; warm
|
|
92
|
+
* ~0.35 s).
|
|
93
|
+
*/
|
|
94
|
+
export async function readClipboardImage(outPath) {
|
|
95
|
+
const c = clipboardImageCommand(process.platform, outPath, process.env);
|
|
96
|
+
if (!c)
|
|
97
|
+
return false;
|
|
98
|
+
const stdout = await new Promise((resolve, reject) => {
|
|
99
|
+
const child = spawn(c.cmd, c.args, { stdio: ['ignore', 'pipe', 'pipe'], windowsHide: true });
|
|
100
|
+
const chunks = [];
|
|
101
|
+
const timer = setTimeout(() => {
|
|
102
|
+
child.kill();
|
|
103
|
+
reject(new Error('clipboard read timed out'));
|
|
104
|
+
}, 10_000);
|
|
105
|
+
child.stdout.on('data', (d) => chunks.push(d));
|
|
106
|
+
child.on('error', (err) => {
|
|
107
|
+
clearTimeout(timer);
|
|
108
|
+
reject(err);
|
|
109
|
+
});
|
|
110
|
+
child.on('close', (code) => {
|
|
111
|
+
clearTimeout(timer);
|
|
112
|
+
if (code === 0)
|
|
113
|
+
resolve(Buffer.concat(chunks));
|
|
114
|
+
else
|
|
115
|
+
reject(new Error(`${c.cmd} exited ${code}`));
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
if (c.stdoutToFile) {
|
|
119
|
+
if (stdout.length === 0)
|
|
120
|
+
return false;
|
|
121
|
+
await fs.writeFile(outPath, stdout);
|
|
122
|
+
return true;
|
|
123
|
+
}
|
|
124
|
+
return stdout.toString('utf8').trim() === 'ok';
|
|
125
|
+
}
|
package/dist/ui/footer.js
CHANGED
|
@@ -5,6 +5,7 @@ import { Box, Text, useInput, useStdout } from 'ink';
|
|
|
5
5
|
// ink-text-input — multi-chunk pastes were losing every chunk but the
|
|
6
6
|
// last (stale-prop closure in the upstream component). See text-input.tsx.
|
|
7
7
|
import TextInput from './text-input.js';
|
|
8
|
+
import { pasteClipboardImage } from './clipboard-image.js';
|
|
8
9
|
import Fuse from 'fuse.js';
|
|
9
10
|
// Task 8 (ux-wave1) — persistence lives in the shared writer (one file,
|
|
10
11
|
// one dedup guard for the Footer, Ink handleSubmit, and classic readline).
|
|
@@ -194,6 +195,15 @@ export function Footer({ onSubmit, onSlashTrigger, onHistoryTrigger, onValueChan
|
|
|
194
195
|
}
|
|
195
196
|
onSubmit(line.trim());
|
|
196
197
|
}, [onSubmit, onSlashTrigger, onHistoryTrigger, history, updateBuffer]);
|
|
198
|
+
// Claude Code-style image paste (2026-09-23): Ctrl+V / Alt+V saves the
|
|
199
|
+
// clipboard image and returns an [Image #N] chip; the turn loop swaps the
|
|
200
|
+
// chip for the file path before the image attachment step.
|
|
201
|
+
const handlePasteImage = useCallback(async () => {
|
|
202
|
+
const chip = await pasteClipboardImage();
|
|
203
|
+
if (!chip)
|
|
204
|
+
onNotice?.('no image on the clipboard');
|
|
205
|
+
return chip;
|
|
206
|
+
}, [onNotice]);
|
|
197
207
|
if (fuzzyMode) {
|
|
198
208
|
return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1, children: [_jsx(Text, { bold: true, color: "cyan", children: "History search (Ctrl+R)" }), _jsx(TextInput, { value: fuzzyQuery, onChange: setFuzzyQuery, placeholder: "type to search...", maxDisplayWidth: Math.max(8, columns - 5) }), fuzzyResults.map((r, i) => (_jsx(Text, { dimColor: i > 0, wrap: "truncate-end", children: flattenHistoryEntry(r) }, i))), _jsx(Text, { dimColor: true, children: "Esc: cancel Enter: select" })] }));
|
|
199
209
|
}
|
|
@@ -205,5 +215,5 @@ export function Footer({ onSubmit, onSlashTrigger, onHistoryTrigger, onValueChan
|
|
|
205
215
|
if (disabled) {
|
|
206
216
|
return (_jsxs(Box, { children: [_jsx(Text, { bold: true, color: PEACH, children: PROMPT }), _jsx(Text, { dimColor: true, children: "(answer above to continue)" })] }));
|
|
207
217
|
}
|
|
208
|
-
return (_jsxs(Box, { flexDirection: "column", children: [buffer.length > 0 && (_jsxs(Box, { flexDirection: "column", children: [buffer.length > HELD_LINES_SHOWN && (_jsxs(Text, { dimColor: true, wrap: "truncate-end", children: ["\u2026 ", buffer.length - HELD_LINES_SHOWN, " earlier line", buffer.length - HELD_LINES_SHOWN === 1 ? '' : 's'] })), buffer.slice(-HELD_LINES_SHOWN).map((line, i) => (_jsxs(Text, { dimColor: true, wrap: "truncate-end", children: ['│ ', line] }, `held-${buffer.length}-${i}`))), _jsxs(Text, { dimColor: true, wrap: "truncate-end", children: [buffer.length, " line", buffer.length === 1 ? '' : 's', " pending \u2014 Enter sends all, Esc discards, \\ continues"] })] })), _jsxs(Box, { children: [_jsx(Text, { bold: true, color: PEACH, children: PROMPT }), _jsx(TextInput, { value: value, onChange: setValue, onSubmit: handleSubmit, onPasteLines: handlePasteLines, maxDisplayWidth: inputBudget })] })] }));
|
|
218
|
+
return (_jsxs(Box, { flexDirection: "column", children: [buffer.length > 0 && (_jsxs(Box, { flexDirection: "column", children: [buffer.length > HELD_LINES_SHOWN && (_jsxs(Text, { dimColor: true, wrap: "truncate-end", children: ["\u2026 ", buffer.length - HELD_LINES_SHOWN, " earlier line", buffer.length - HELD_LINES_SHOWN === 1 ? '' : 's'] })), buffer.slice(-HELD_LINES_SHOWN).map((line, i) => (_jsxs(Text, { dimColor: true, wrap: "truncate-end", children: ['│ ', line] }, `held-${buffer.length}-${i}`))), _jsxs(Text, { dimColor: true, wrap: "truncate-end", children: [buffer.length, " line", buffer.length === 1 ? '' : 's', " pending \u2014 Enter sends all, Esc discards, \\ continues"] })] })), _jsxs(Box, { children: [_jsx(Text, { bold: true, color: PEACH, children: PROMPT }), _jsx(TextInput, { value: value, onChange: setValue, onSubmit: handleSubmit, onPasteLines: handlePasteLines, onPasteImage: handlePasteImage, maxDisplayWidth: inputBudget })] })] }));
|
|
209
219
|
}
|
package/dist/ui/text-input.js
CHANGED
|
@@ -27,6 +27,8 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
27
27
|
import { useState, useEffect, useRef } from 'react';
|
|
28
28
|
import { Text, useInput } from 'ink';
|
|
29
29
|
import chalk from 'chalk';
|
|
30
|
+
/** An image chip ending exactly at the cursor — Backspace removes it whole. */
|
|
31
|
+
const CHIP_BEFORE_CURSOR = /\[Image #\d+\]$/;
|
|
30
32
|
/**
|
|
31
33
|
* Pure windowing core (exported for tests). Given the value length, the
|
|
32
34
|
* cursor position, the display budget (character cells) and whether the
|
|
@@ -48,7 +50,7 @@ export function computeDisplayWindow(valueLength, cursorOffset, budget, cursorVi
|
|
|
48
50
|
const end = Math.min(valueLength, start + budget);
|
|
49
51
|
return { start, end };
|
|
50
52
|
}
|
|
51
|
-
export function PasteSafeTextInput({ value: originalValue, placeholder = '', focus = true, mask, showCursor = true, onChange, onSubmit, onPasteLines, maxDisplayWidth, }) {
|
|
53
|
+
export function PasteSafeTextInput({ value: originalValue, placeholder = '', focus = true, mask, showCursor = true, onChange, onSubmit, onPasteLines, maxDisplayWidth, onPasteImage, }) {
|
|
52
54
|
const [cursorOffset, setCursorOffset] = useState((originalValue || '').length);
|
|
53
55
|
// The paste-safety core: synchronous mirrors of value + cursor. The
|
|
54
56
|
// input handler reads/writes THESE, never the (possibly stale) props.
|
|
@@ -129,6 +131,23 @@ export function PasteSafeTextInput({ value: originalValue, placeholder = '', foc
|
|
|
129
131
|
(key.shift && key.tab)) {
|
|
130
132
|
return;
|
|
131
133
|
}
|
|
134
|
+
if (onPasteImage && input === 'v' && (key.ctrl || key.meta)) {
|
|
135
|
+
void onPasteImage().then((chip) => {
|
|
136
|
+
if (!chip)
|
|
137
|
+
return;
|
|
138
|
+
// Resolves after the clipboard read — insert at the cursor as it
|
|
139
|
+
// is NOW, through the same synchronous mirrors as a keystroke.
|
|
140
|
+
const cur = valueRef.current;
|
|
141
|
+
const at = cursorRef.current;
|
|
142
|
+
const next = cur.slice(0, at) + chip + cur.slice(at);
|
|
143
|
+
valueRef.current = next;
|
|
144
|
+
cursorRef.current = at + chip.length;
|
|
145
|
+
setCursorOffset(at + chip.length);
|
|
146
|
+
pendingEchoes.current.push(next);
|
|
147
|
+
onChange(next);
|
|
148
|
+
});
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
132
151
|
if (key.return) {
|
|
133
152
|
// valueRef (not the prop) — so an Enter in the same stdin drain as
|
|
134
153
|
// the paste tail submits the complete line.
|
|
@@ -151,7 +170,16 @@ export function PasteSafeTextInput({ value: originalValue, placeholder = '', foc
|
|
|
151
170
|
nextCursorOffset++;
|
|
152
171
|
}
|
|
153
172
|
else if (key.backspace || key.delete) {
|
|
154
|
-
|
|
173
|
+
const chip = onPasteImage
|
|
174
|
+
? CHIP_BEFORE_CURSOR.exec(currentValue.slice(0, cursorRef.current))
|
|
175
|
+
: null;
|
|
176
|
+
if (chip) {
|
|
177
|
+
nextValue =
|
|
178
|
+
currentValue.slice(0, cursorRef.current - chip[0].length) +
|
|
179
|
+
currentValue.slice(cursorRef.current);
|
|
180
|
+
nextCursorOffset -= chip[0].length;
|
|
181
|
+
}
|
|
182
|
+
else if (cursorRef.current > 0) {
|
|
155
183
|
nextValue =
|
|
156
184
|
currentValue.slice(0, cursorRef.current - 1) +
|
|
157
185
|
currentValue.slice(cursorRef.current, currentValue.length);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cspeach/cli",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.16",
|
|
4
4
|
"description": "CSPeach — AI-assisted ABAP development CLI. Direct SAP access, 34 skills, safety gates.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"abap",
|
|
@@ -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+') — 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",
|