@vincemakes/kiso-code 0.15.5 → 0.15.7
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/dist/attachments.d.ts +41 -0
- package/dist/attachments.js +122 -0
- package/dist/byte-trace.d.ts +46 -0
- package/dist/byte-trace.js +103 -0
- package/dist/chat.js +8 -1
- package/dist/clipboard.d.ts +37 -0
- package/dist/clipboard.js +70 -0
- package/dist/index.js +21 -0
- package/dist/state.d.ts +5 -0
- package/package.json +14 -14
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* REL-0152-D11 — an image reaches the model.
|
|
3
|
+
*
|
|
4
|
+
* The wire was already complete and nobody had noticed. The `user_input`
|
|
5
|
+
* event's content has been `string | ContentBlock[]` since the protocol
|
|
6
|
+
* was written; the projection passes it through untouched; the Anthropic
|
|
7
|
+
* adapter sends an `image` block and the OpenAI-compatible family sends
|
|
8
|
+
* `image_url` with a data URI. What was missing was at the two ends:
|
|
9
|
+
* `session.run()` narrowed its parameter to text, and the CLI never
|
|
10
|
+
* built a block — `grep 'type: "image"' apps/cli/src` came back empty.
|
|
11
|
+
*
|
|
12
|
+
* This module is the CLI end. A turn's text is scanned for references to
|
|
13
|
+
* image files and each one becomes an image block beside the words.
|
|
14
|
+
*
|
|
15
|
+
* The reference is a PATH, deliberately, because two things a user can
|
|
16
|
+
* already do produce one on any terminal with no clipboard involved:
|
|
17
|
+
* dragging a file into the window, which terminals answer by inserting
|
|
18
|
+
* its path, and typing or pasting a path. Clipboard image paste is a
|
|
19
|
+
* separate mechanism with a separate open question (see the finding);
|
|
20
|
+
* this one works today, everywhere, and is what that mechanism will
|
|
21
|
+
* hand its temp file to when it lands.
|
|
22
|
+
*/
|
|
23
|
+
import type { ContentBlock } from "@vincemakes/kiso-core";
|
|
24
|
+
/** The four media types the protocol accepts, recognised by the file's
|
|
25
|
+
* own leading bytes. A name is a claim; the bytes are the fact, and a
|
|
26
|
+
* screenshot saved as `.jpg` is a very ordinary thing to have. */
|
|
27
|
+
export declare function sniff(buf: Buffer): "image/png" | "image/jpeg" | "image/gif" | "image/webp" | null;
|
|
28
|
+
/**
|
|
29
|
+
* The turn's content: the string itself when nothing was attached, or
|
|
30
|
+
* the blocks when something was.
|
|
31
|
+
*
|
|
32
|
+
* Returning the STRING unchanged in the common case is not an
|
|
33
|
+
* optimisation, it is the compatibility guarantee: a turn with no image
|
|
34
|
+
* produces exactly the bytes it produced before this module existed, so
|
|
35
|
+
* every request that is not about an image is unaffected by the feature.
|
|
36
|
+
*
|
|
37
|
+
* A path that is not there, or is not an image, or is too large, is left
|
|
38
|
+
* standing in the text. Silently dropping it would tell the model about
|
|
39
|
+
* a file it cannot see, which is worse than telling it nothing.
|
|
40
|
+
*/
|
|
41
|
+
export declare function attachImages(text: string): string | ContentBlock[];
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* REL-0152-D11 — an image reaches the model.
|
|
3
|
+
*
|
|
4
|
+
* The wire was already complete and nobody had noticed. The `user_input`
|
|
5
|
+
* event's content has been `string | ContentBlock[]` since the protocol
|
|
6
|
+
* was written; the projection passes it through untouched; the Anthropic
|
|
7
|
+
* adapter sends an `image` block and the OpenAI-compatible family sends
|
|
8
|
+
* `image_url` with a data URI. What was missing was at the two ends:
|
|
9
|
+
* `session.run()` narrowed its parameter to text, and the CLI never
|
|
10
|
+
* built a block — `grep 'type: "image"' apps/cli/src` came back empty.
|
|
11
|
+
*
|
|
12
|
+
* This module is the CLI end. A turn's text is scanned for references to
|
|
13
|
+
* image files and each one becomes an image block beside the words.
|
|
14
|
+
*
|
|
15
|
+
* The reference is a PATH, deliberately, because two things a user can
|
|
16
|
+
* already do produce one on any terminal with no clipboard involved:
|
|
17
|
+
* dragging a file into the window, which terminals answer by inserting
|
|
18
|
+
* its path, and typing or pasting a path. Clipboard image paste is a
|
|
19
|
+
* separate mechanism with a separate open question (see the finding);
|
|
20
|
+
* this one works today, everywhere, and is what that mechanism will
|
|
21
|
+
* hand its temp file to when it lands.
|
|
22
|
+
*/
|
|
23
|
+
import { readFileSync, statSync } from "node:fs";
|
|
24
|
+
/**
|
|
25
|
+
* The size a single image may reach before it is refused.
|
|
26
|
+
*
|
|
27
|
+
* Providers cap image payloads and base64 inflates by a third, so a file
|
|
28
|
+
* near this bound is already near theirs. The refusal is deliberate and
|
|
29
|
+
* VISIBLE: the path stays in the text, so the model is told a file was
|
|
30
|
+
* named rather than being handed a request that the provider will reject
|
|
31
|
+
* with something the user cannot act on.
|
|
32
|
+
*/
|
|
33
|
+
const MAX_IMAGE_BYTES = 5 * 1024 * 1024;
|
|
34
|
+
/** The four media types the protocol accepts, recognised by the file's
|
|
35
|
+
* own leading bytes. A name is a claim; the bytes are the fact, and a
|
|
36
|
+
* screenshot saved as `.jpg` is a very ordinary thing to have. */
|
|
37
|
+
export function sniff(buf) {
|
|
38
|
+
if (buf.length >= 8 && buf.subarray(0, 8).equals(Buffer.from("89504e470d0a1a0a", "hex")))
|
|
39
|
+
return "image/png";
|
|
40
|
+
if (buf.length >= 3 && buf[0] === 0xff && buf[1] === 0xd8 && buf[2] === 0xff)
|
|
41
|
+
return "image/jpeg";
|
|
42
|
+
if (buf.length >= 6 && (buf.subarray(0, 6).toString("latin1") === "GIF87a" || buf.subarray(0, 6).toString("latin1") === "GIF89a"))
|
|
43
|
+
return "image/gif";
|
|
44
|
+
if (buf.length >= 12 && buf.subarray(0, 4).toString("latin1") === "RIFF" && buf.subarray(8, 12).toString("latin1") === "WEBP")
|
|
45
|
+
return "image/webp";
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Path-shaped runs in a line, longest first so a quoted path wins over
|
|
50
|
+
* the bare fragment inside it. Quoted forms come first because that is
|
|
51
|
+
* what a terminal inserts for a name containing a space.
|
|
52
|
+
*/
|
|
53
|
+
const CANDIDATES = [
|
|
54
|
+
/'([^']*\.(?:png|jpe?g|gif|webp))'/gi,
|
|
55
|
+
/"([^"]*\.(?:png|jpe?g|gif|webp))"/gi,
|
|
56
|
+
/(\S*\.(?:png|jpe?g|gif|webp))/gi,
|
|
57
|
+
];
|
|
58
|
+
function look(path) {
|
|
59
|
+
try {
|
|
60
|
+
const st = statSync(path);
|
|
61
|
+
if (!st.isFile() || st.size > MAX_IMAGE_BYTES || st.size === 0)
|
|
62
|
+
return null;
|
|
63
|
+
const buf = readFileSync(path);
|
|
64
|
+
const mediaType = sniff(buf);
|
|
65
|
+
if (mediaType === null)
|
|
66
|
+
return null;
|
|
67
|
+
return { mediaType, data: buf.toString("base64") };
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
return null; // not there, not readable, not ours to complain about
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* The turn's content: the string itself when nothing was attached, or
|
|
75
|
+
* the blocks when something was.
|
|
76
|
+
*
|
|
77
|
+
* Returning the STRING unchanged in the common case is not an
|
|
78
|
+
* optimisation, it is the compatibility guarantee: a turn with no image
|
|
79
|
+
* produces exactly the bytes it produced before this module existed, so
|
|
80
|
+
* every request that is not about an image is unaffected by the feature.
|
|
81
|
+
*
|
|
82
|
+
* A path that is not there, or is not an image, or is too large, is left
|
|
83
|
+
* standing in the text. Silently dropping it would tell the model about
|
|
84
|
+
* a file it cannot see, which is worse than telling it nothing.
|
|
85
|
+
*/
|
|
86
|
+
export function attachImages(text) {
|
|
87
|
+
const found = [];
|
|
88
|
+
const claimed = [];
|
|
89
|
+
for (const re of CANDIDATES) {
|
|
90
|
+
re.lastIndex = 0;
|
|
91
|
+
for (const m of text.matchAll(re)) {
|
|
92
|
+
const path = m[1];
|
|
93
|
+
const start = m.index;
|
|
94
|
+
const end = start + m[0].length;
|
|
95
|
+
if (claimed.some((c) => start < c.end && end > c.start))
|
|
96
|
+
continue;
|
|
97
|
+
const hit = look(path);
|
|
98
|
+
if (hit === null)
|
|
99
|
+
continue;
|
|
100
|
+
claimed.push({ start, end });
|
|
101
|
+
found.push({ start, end, block: { type: "image", sourceType: "base64", mediaType: hit.mediaType, data: hit.data } });
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
if (found.length === 0)
|
|
105
|
+
return text;
|
|
106
|
+
found.sort((a, b) => a.start - b.start);
|
|
107
|
+
// the words are kept and kept IN PLACE: a turn is "what is wrong
|
|
108
|
+
// here?" plus the screenshot, and the question is half of it.
|
|
109
|
+
const blocks = [];
|
|
110
|
+
let at = 0;
|
|
111
|
+
for (const f of found) {
|
|
112
|
+
const before = text.slice(at, f.start).trim();
|
|
113
|
+
if (before !== "")
|
|
114
|
+
blocks.push({ type: "text", text: before });
|
|
115
|
+
blocks.push(f.block);
|
|
116
|
+
at = f.end;
|
|
117
|
+
}
|
|
118
|
+
const tail = text.slice(at).trim();
|
|
119
|
+
if (tail !== "")
|
|
120
|
+
blocks.push({ type: "text", text: tail });
|
|
121
|
+
return blocks;
|
|
122
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* REL-0152-D12 — the byte trace: what kiso actually sent, and when.
|
|
3
|
+
*
|
|
4
|
+
* Three defects in this round are only decidable from the byte stream:
|
|
5
|
+
*
|
|
6
|
+
* - REL-0152-D1, the stray `[` and `]` at the row edges. `[` is the
|
|
7
|
+
* CSI introducer and `]` the OSC introducer, so a lost ESC prints
|
|
8
|
+
* exactly those two characters and nothing else — but kiso emits no
|
|
9
|
+
* OSC at all, and every CSI it writes carries more than its
|
|
10
|
+
* introducer. Either the bytes leaving kiso already contain those
|
|
11
|
+
* characters (ours) or they do not (the terminal's parser). Nobody
|
|
12
|
+
* can tell from a screenshot, and three rounds have now tried.
|
|
13
|
+
* - REL-0152-D7, content that appears late or not at all. The frame
|
|
14
|
+
* that should have carried it either went out or did not.
|
|
15
|
+
* - the paste latency. The editor is measured at 146KB in 6ms, so if
|
|
16
|
+
* a paste still feels slow the time is being spent before kiso sees
|
|
17
|
+
* the bytes — which the arrival timestamps show directly.
|
|
18
|
+
*
|
|
19
|
+
* Set KISO_TRACE_BYTES to a path and every byte in BOTH directions is
|
|
20
|
+
* recorded with a millisecond stamp, then the file answers the question
|
|
21
|
+
* instead of another round of inference.
|
|
22
|
+
*
|
|
23
|
+
* KISO_TRACE_BYTES=/tmp/kiso-bytes.jsonl kiso chat
|
|
24
|
+
*
|
|
25
|
+
* One JSON object per line: {ms, dir, n, b} — the stamp relative to the
|
|
26
|
+
* trace's start, "out" or "in", the byte count, and the bytes as base64
|
|
27
|
+
* so no escape, control character or partial UTF-8 sequence is altered
|
|
28
|
+
* on the way to disk. Nothing is interpreted here; interpretation is
|
|
29
|
+
* what the trace exists to make possible.
|
|
30
|
+
*
|
|
31
|
+
* OFF unless the variable is set: zero cost, and it never turns itself
|
|
32
|
+
* on. What it records is the terminal traffic of one session — screen
|
|
33
|
+
* output and keystrokes — so it is a diagnostic a person switches on
|
|
34
|
+
* deliberately, for one run, when something is wrong.
|
|
35
|
+
*/
|
|
36
|
+
/**
|
|
37
|
+
* Arm the trace if KISO_TRACE_BYTES names a file. Call once, before the
|
|
38
|
+
* dock is entered, so the first frame is in the record too.
|
|
39
|
+
*
|
|
40
|
+
* stdout is wrapped rather than tapped because there is no other way to
|
|
41
|
+
* see every write: the compositor, the CLI's own logging and node's
|
|
42
|
+
* error paths all reach the terminal through it. stdin is TAPPED — an
|
|
43
|
+
* extra "data" listener, which every listener receives — so the editor's
|
|
44
|
+
* own reading is untouched and the trace cannot swallow a keystroke.
|
|
45
|
+
*/
|
|
46
|
+
export declare function armByteTrace(): void;
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* REL-0152-D12 — the byte trace: what kiso actually sent, and when.
|
|
3
|
+
*
|
|
4
|
+
* Three defects in this round are only decidable from the byte stream:
|
|
5
|
+
*
|
|
6
|
+
* - REL-0152-D1, the stray `[` and `]` at the row edges. `[` is the
|
|
7
|
+
* CSI introducer and `]` the OSC introducer, so a lost ESC prints
|
|
8
|
+
* exactly those two characters and nothing else — but kiso emits no
|
|
9
|
+
* OSC at all, and every CSI it writes carries more than its
|
|
10
|
+
* introducer. Either the bytes leaving kiso already contain those
|
|
11
|
+
* characters (ours) or they do not (the terminal's parser). Nobody
|
|
12
|
+
* can tell from a screenshot, and three rounds have now tried.
|
|
13
|
+
* - REL-0152-D7, content that appears late or not at all. The frame
|
|
14
|
+
* that should have carried it either went out or did not.
|
|
15
|
+
* - the paste latency. The editor is measured at 146KB in 6ms, so if
|
|
16
|
+
* a paste still feels slow the time is being spent before kiso sees
|
|
17
|
+
* the bytes — which the arrival timestamps show directly.
|
|
18
|
+
*
|
|
19
|
+
* Set KISO_TRACE_BYTES to a path and every byte in BOTH directions is
|
|
20
|
+
* recorded with a millisecond stamp, then the file answers the question
|
|
21
|
+
* instead of another round of inference.
|
|
22
|
+
*
|
|
23
|
+
* KISO_TRACE_BYTES=/tmp/kiso-bytes.jsonl kiso chat
|
|
24
|
+
*
|
|
25
|
+
* One JSON object per line: {ms, dir, n, b} — the stamp relative to the
|
|
26
|
+
* trace's start, "out" or "in", the byte count, and the bytes as base64
|
|
27
|
+
* so no escape, control character or partial UTF-8 sequence is altered
|
|
28
|
+
* on the way to disk. Nothing is interpreted here; interpretation is
|
|
29
|
+
* what the trace exists to make possible.
|
|
30
|
+
*
|
|
31
|
+
* OFF unless the variable is set: zero cost, and it never turns itself
|
|
32
|
+
* on. What it records is the terminal traffic of one session — screen
|
|
33
|
+
* output and keystrokes — so it is a diagnostic a person switches on
|
|
34
|
+
* deliberately, for one run, when something is wrong.
|
|
35
|
+
*/
|
|
36
|
+
import { appendFileSync, writeFileSync } from "node:fs";
|
|
37
|
+
let path = null;
|
|
38
|
+
let start = 0;
|
|
39
|
+
function line(dir, data) {
|
|
40
|
+
if (path === null)
|
|
41
|
+
return;
|
|
42
|
+
const buf = typeof data === "string" ? Buffer.from(data, "utf8") : Buffer.from(data);
|
|
43
|
+
try {
|
|
44
|
+
appendFileSync(path, `${JSON.stringify({ ms: Date.now() - start, dir, n: buf.length, b: buf.toString("base64") })}\n`);
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
// a trace that fails must never take the session with it
|
|
48
|
+
path = null;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Arm the trace if KISO_TRACE_BYTES names a file. Call once, before the
|
|
53
|
+
* dock is entered, so the first frame is in the record too.
|
|
54
|
+
*
|
|
55
|
+
* stdout is wrapped rather than tapped because there is no other way to
|
|
56
|
+
* see every write: the compositor, the CLI's own logging and node's
|
|
57
|
+
* error paths all reach the terminal through it. stdin is TAPPED — an
|
|
58
|
+
* extra "data" listener, which every listener receives — so the editor's
|
|
59
|
+
* own reading is untouched and the trace cannot swallow a keystroke.
|
|
60
|
+
*/
|
|
61
|
+
export function armByteTrace() {
|
|
62
|
+
const target = process.env.KISO_TRACE_BYTES;
|
|
63
|
+
if (target === undefined || target === "")
|
|
64
|
+
return;
|
|
65
|
+
path = target;
|
|
66
|
+
start = Date.now();
|
|
67
|
+
try {
|
|
68
|
+
writeFileSync(path, "");
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
path = null;
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
const write = process.stdout.write.bind(process.stdout);
|
|
75
|
+
process.stdout.write = ((chunk, ...rest) => {
|
|
76
|
+
line("out", chunk);
|
|
77
|
+
return write(chunk, ...rest);
|
|
78
|
+
});
|
|
79
|
+
process.stdin.on("data", (chunk) => line("in", chunk));
|
|
80
|
+
// The environment goes in the record FIRST, because kiso emits
|
|
81
|
+
// DIFFERENT frame bytes per terminal — DEC 2026 synchronized output
|
|
82
|
+
// where it is supported, a cursor-hide degrade on Apple Terminal —
|
|
83
|
+
// so a capture that does not say which terminal made it cannot be
|
|
84
|
+
// compared against anything. Same for the size and the locale: a row
|
|
85
|
+
// is built to a width, and the ambiguous-width characters kiso uses
|
|
86
|
+
// in its own chrome (· … —) are one cell or two depending on how the
|
|
87
|
+
// terminal was told to treat them.
|
|
88
|
+
const env = {
|
|
89
|
+
TERM_PROGRAM: process.env.TERM_PROGRAM ?? null,
|
|
90
|
+
TERM_PROGRAM_VERSION: process.env.TERM_PROGRAM_VERSION ?? null,
|
|
91
|
+
TERM: process.env.TERM ?? null,
|
|
92
|
+
LANG: process.env.LANG ?? null,
|
|
93
|
+
LC_CTYPE: process.env.LC_CTYPE ?? null,
|
|
94
|
+
columns: process.stdout.columns ?? null,
|
|
95
|
+
rows: process.stdout.rows ?? null,
|
|
96
|
+
};
|
|
97
|
+
try {
|
|
98
|
+
appendFileSync(path, `${JSON.stringify({ ms: 0, dir: "env", n: 0, b: "", env })}\n`);
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
path = null;
|
|
102
|
+
}
|
|
103
|
+
}
|
package/dist/chat.js
CHANGED
|
@@ -12,6 +12,7 @@ import { canonicalizeUsage } from "@vincemakes/kiso-runtime";
|
|
|
12
12
|
import { canonicalizeUsageForModel } from "@vincemakes/kiso-runtime/internal";
|
|
13
13
|
import { dispatch } from "./dispatch.js";
|
|
14
14
|
import { agentModel, body, bodyLog, configuredWindow, dock } from "./state.js";
|
|
15
|
+
import { attachImages } from "./attachments.js";
|
|
15
16
|
import { lookupModelMetadata } from "@vincemakes/kiso-runtime/internal";
|
|
16
17
|
import { addDontAskAgainRule, askPanel, fixHintFor, pendingAsk, resolveUncertains } from "./trust-ui.js";
|
|
17
18
|
import { FauxExhaustionError, failOnFauxExhaustion } from "./faux-glue.js";
|
|
@@ -822,7 +823,13 @@ export async function chat(session, faux, input, autoCompact, nav) {
|
|
|
822
823
|
};
|
|
823
824
|
const turn = (text, seedSource) => new Promise((resolve, reject) => {
|
|
824
825
|
queued = Math.max(0, queued - 1); // a queued turn starts
|
|
825
|
-
|
|
826
|
+
// REL-0152-D11: a turn that names an image file carries it. The
|
|
827
|
+
// scan returns the STRING unchanged when it finds nothing, so a
|
|
828
|
+
// turn without one is byte-identical to before the feature.
|
|
829
|
+
// Seeded turns are the product's own words and are never
|
|
830
|
+
// scanned — nothing it writes to itself is an attachment.
|
|
831
|
+
const content = seedSource !== undefined ? text : attachImages(text);
|
|
832
|
+
const run = seedSource !== undefined ? session.run(content, { source: seedSource }) : session.run(content);
|
|
826
833
|
currentRun = run;
|
|
827
834
|
turnNo += 1;
|
|
828
835
|
const myTurn = turnNo;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* REL-0152-D11 — reading an image off the clipboard.
|
|
3
|
+
*
|
|
4
|
+
* SPLIT from attachments.ts on purpose. The pty-manifest gate
|
|
5
|
+
* classifies a test by its resource-dependency CLOSURE, and a module
|
|
6
|
+
* that spawns drags every test that imports it into the serial pool.
|
|
7
|
+
* The path scanning and the block building are pure and belong in the
|
|
8
|
+
* fast pool; only this file needs a process, so only this file's tests
|
|
9
|
+
* pay for one. The gate found that out, which is what it is for.
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* REL-0152-D11, the clipboard half.
|
|
13
|
+
*
|
|
14
|
+
* Pasting an image into a terminal sends NOTHING useful: the terminal
|
|
15
|
+
* has no way to put binary into a byte stream, so a bracketed paste
|
|
16
|
+
* arrives empty (or with a filename, depending on the source app). An
|
|
17
|
+
* empty paste is therefore the signal — the user pressed paste and the
|
|
18
|
+
* terminal had nothing to give us — and the image, if there is one, has
|
|
19
|
+
* to be fetched from the clipboard directly.
|
|
20
|
+
*
|
|
21
|
+
* macOS only for now, through `osascript`, which is present on every
|
|
22
|
+
* Mac and needs no dependency. The coercion `the clipboard as «class
|
|
23
|
+
* PNGf»` is the documented way to get PNG bytes out of the pasteboard.
|
|
24
|
+
*
|
|
25
|
+
* KNOWN UNRESOLVED, and stated here rather than discovered later: that
|
|
26
|
+
* coercion FAILS with error -1700 when osascript runs detached from the
|
|
27
|
+
* user's session, which is where this was developed. `clipboard info`
|
|
28
|
+
* correctly reports the PNG flavour is present, so it is not a
|
|
29
|
+
* permission wall — it has the shape of a promised (lazily rendered)
|
|
30
|
+
* flavour that the source app materialises only for a process in the
|
|
31
|
+
* right session context. kiso runs in the user's own terminal session,
|
|
32
|
+
* where it may simply work. This returns null on any failure and the
|
|
33
|
+
* caller says so out loud; nothing here guesses.
|
|
34
|
+
*/
|
|
35
|
+
export declare function clipboardImage(dir: string, run?: (cmd: string, args: readonly string[]) => {
|
|
36
|
+
status: number | null;
|
|
37
|
+
}): string | null;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* REL-0152-D11 — reading an image off the clipboard.
|
|
3
|
+
*
|
|
4
|
+
* SPLIT from attachments.ts on purpose. The pty-manifest gate
|
|
5
|
+
* classifies a test by its resource-dependency CLOSURE, and a module
|
|
6
|
+
* that spawns drags every test that imports it into the serial pool.
|
|
7
|
+
* The path scanning and the block building are pure and belong in the
|
|
8
|
+
* fast pool; only this file needs a process, so only this file's tests
|
|
9
|
+
* pay for one. The gate found that out, which is what it is for.
|
|
10
|
+
*/
|
|
11
|
+
import { readFileSync, statSync } from "node:fs";
|
|
12
|
+
import { spawnSync } from "node:child_process";
|
|
13
|
+
import { join } from "node:path";
|
|
14
|
+
import { sniff } from "./attachments.js";
|
|
15
|
+
/**
|
|
16
|
+
* REL-0152-D11, the clipboard half.
|
|
17
|
+
*
|
|
18
|
+
* Pasting an image into a terminal sends NOTHING useful: the terminal
|
|
19
|
+
* has no way to put binary into a byte stream, so a bracketed paste
|
|
20
|
+
* arrives empty (or with a filename, depending on the source app). An
|
|
21
|
+
* empty paste is therefore the signal — the user pressed paste and the
|
|
22
|
+
* terminal had nothing to give us — and the image, if there is one, has
|
|
23
|
+
* to be fetched from the clipboard directly.
|
|
24
|
+
*
|
|
25
|
+
* macOS only for now, through `osascript`, which is present on every
|
|
26
|
+
* Mac and needs no dependency. The coercion `the clipboard as «class
|
|
27
|
+
* PNGf»` is the documented way to get PNG bytes out of the pasteboard.
|
|
28
|
+
*
|
|
29
|
+
* KNOWN UNRESOLVED, and stated here rather than discovered later: that
|
|
30
|
+
* coercion FAILS with error -1700 when osascript runs detached from the
|
|
31
|
+
* user's session, which is where this was developed. `clipboard info`
|
|
32
|
+
* correctly reports the PNG flavour is present, so it is not a
|
|
33
|
+
* permission wall — it has the shape of a promised (lazily rendered)
|
|
34
|
+
* flavour that the source app materialises only for a process in the
|
|
35
|
+
* right session context. kiso runs in the user's own terminal session,
|
|
36
|
+
* where it may simply work. This returns null on any failure and the
|
|
37
|
+
* caller says so out loud; nothing here guesses.
|
|
38
|
+
*/
|
|
39
|
+
export function clipboardImage(dir, run = defaultRun) {
|
|
40
|
+
if (process.platform !== "darwin")
|
|
41
|
+
return null;
|
|
42
|
+
const target = join(dir, `paste-${process.pid}-${Date.now()}.png`);
|
|
43
|
+
const script = [
|
|
44
|
+
"set d to (the clipboard as «class PNGf»)",
|
|
45
|
+
`set f to open for access POSIX file ${JSON.stringify(target)} with write permission`,
|
|
46
|
+
"set eof f to 0",
|
|
47
|
+
"write d to f",
|
|
48
|
+
"close access f",
|
|
49
|
+
].flatMap((line) => ["-e", line]);
|
|
50
|
+
try {
|
|
51
|
+
const r = run("osascript", script);
|
|
52
|
+
if (r.status !== 0)
|
|
53
|
+
return null;
|
|
54
|
+
// the file must be a real image by the SAME sniffer the path
|
|
55
|
+
// route uses — a zero-byte file from a half-failed coercion is
|
|
56
|
+
// exactly what this round already produced once
|
|
57
|
+
const st = statSync(target);
|
|
58
|
+
if (!st.isFile() || st.size === 0)
|
|
59
|
+
return null;
|
|
60
|
+
if (sniff(readFileSync(target)) === null)
|
|
61
|
+
return null;
|
|
62
|
+
return target;
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
function defaultRun(cmd, args) {
|
|
69
|
+
return spawnSync(cmd, [...args], { stdio: "ignore", timeout: 5000 });
|
|
70
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -42,6 +42,9 @@ import { autoCompactFromEnv, chat, contextWindowTokens, estimateCtxRatio } from
|
|
|
42
42
|
import { loadProjectConfig, loadUserConfig, mergeConfigs, resolveAutoCompact, resolveContextWindow, resolveModel } from "./config.js";
|
|
43
43
|
import { resume } from "./resume.js";
|
|
44
44
|
import { resumeTail } from "./resume-tail.js";
|
|
45
|
+
import { armByteTrace } from "./byte-trace.js";
|
|
46
|
+
import { tmpdir } from "node:os";
|
|
47
|
+
import { clipboardImage } from "./clipboard.js";
|
|
45
48
|
import { collectSessionCards, projectSessionCard } from "./session-cards.js";
|
|
46
49
|
// The moved exports stay reachable from this entry — the test imports
|
|
47
50
|
// (project-trust, coding-agent) never change (B4: zero assertion changes).
|
|
@@ -165,6 +168,9 @@ function editorInput(editor) {
|
|
|
165
168
|
onRedirect(cb) {
|
|
166
169
|
editor.onRedirect(cb);
|
|
167
170
|
},
|
|
171
|
+
onEmptyPaste(cb) {
|
|
172
|
+
editor.onEmptyPaste(cb);
|
|
173
|
+
},
|
|
168
174
|
question(query, cb) {
|
|
169
175
|
editor.question(query, cb);
|
|
170
176
|
},
|
|
@@ -630,6 +636,9 @@ async function main() {
|
|
|
630
636
|
// swallows it — the standard "the stream may die under me" idiom.
|
|
631
637
|
process.stdout.on("error", () => { });
|
|
632
638
|
process.stderr.on("error", () => { });
|
|
639
|
+
// REL-0152-D12: armed only by KISO_TRACE_BYTES, and armed HERE so the
|
|
640
|
+
// banner and the first frame are in the record. Off by default.
|
|
641
|
+
armByteTrace();
|
|
633
642
|
// Modes: --mode <name> wins over KISO_MODE — both applied before the
|
|
634
643
|
// first makeAgent (the tier extensions read `current` live). The flag
|
|
635
644
|
// is stripped from the positional args, so it works in any position.
|
|
@@ -755,6 +764,18 @@ async function main() {
|
|
|
755
764
|
editCol: () => dock.editCol(),
|
|
756
765
|
onDock: () => dock.redraw(), // v2d-B: the freeze scrolls the dock up — re-pin it
|
|
757
766
|
}));
|
|
767
|
+
// REL-0152-D11: pasting an image sends no bytes, so an empty paste is
|
|
768
|
+
// the signal to go and look at the clipboard. What comes back is a
|
|
769
|
+
// PATH, which the turn's attachment scan then picks up exactly as it
|
|
770
|
+
// would a dragged-in file — one mechanism, two ways of naming a file.
|
|
771
|
+
input.onEmptyPaste?.(() => {
|
|
772
|
+
const shot = clipboardImage(tmpdir());
|
|
773
|
+
if (shot === null) {
|
|
774
|
+
bodyLog("[nothing on the clipboard that kiso can read as an image]");
|
|
775
|
+
return null;
|
|
776
|
+
}
|
|
777
|
+
return shot;
|
|
778
|
+
});
|
|
758
779
|
try {
|
|
759
780
|
// merge round B: the project config's mode applies AFTER the trust gate
|
|
760
781
|
// (its verdict decides whether the project config exists at all) —
|
package/dist/state.d.ts
CHANGED
|
@@ -75,6 +75,11 @@ export interface LineInput {
|
|
|
75
75
|
* to stop. OPTIONAL: the pipe path has no raw keys and never wires
|
|
76
76
|
* it, so readline stays exactly as it was. */
|
|
77
77
|
onRedirect?(cb: (line: string) => void): void;
|
|
78
|
+
/** REL-0152-D11: an empty bracketed paste is the image case — a
|
|
79
|
+
* terminal cannot put binary in a byte stream, so pasting one sends
|
|
80
|
+
* nothing. The callback returns text to insert (a path) or null.
|
|
81
|
+
* Optional: the pipe path has no editor and no clipboard. */
|
|
82
|
+
onEmptyPaste?(cb: () => string | null): void;
|
|
78
83
|
question(query: string, cb: (answer: string) => void): void;
|
|
79
84
|
cancelQuestion(): void;
|
|
80
85
|
/** W21: open the approval panel — the editor's state machine takes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vincemakes/kiso-code",
|
|
3
|
-
"version": "0.15.
|
|
3
|
+
"version": "0.15.7",
|
|
4
4
|
"description": "kiso CLI \u2014 the durable coding agent that survives kill -9: kiso chat / kiso resume / kiso sessions.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -18,19 +18,19 @@
|
|
|
18
18
|
"test": "vitest run"
|
|
19
19
|
},
|
|
20
20
|
"dependencies": {
|
|
21
|
-
"@vincemakes/kiso-ask-ext": "0.15.
|
|
22
|
-
"@vincemakes/kiso-core": "0.15.
|
|
23
|
-
"@vincemakes/kiso-evals": "0.15.
|
|
24
|
-
"@vincemakes/kiso-mcp-ext": "0.15.
|
|
25
|
-
"@vincemakes/kiso-provider-anthropic": "0.15.
|
|
26
|
-
"@vincemakes/kiso-provider-openai": "0.15.
|
|
27
|
-
"@vincemakes/kiso-runtime": "0.15.
|
|
28
|
-
"@vincemakes/kiso-skills-ext": "0.15.
|
|
29
|
-
"@vincemakes/kiso-subagent-ext": "0.15.
|
|
30
|
-
"@vincemakes/kiso-task-ext": "0.15.
|
|
31
|
-
"@vincemakes/kiso-tools-node": "0.15.
|
|
32
|
-
"@vincemakes/kiso-tui": "0.15.
|
|
33
|
-
"@vincemakes/kiso-tui-cells": "0.15.
|
|
21
|
+
"@vincemakes/kiso-ask-ext": "0.15.7",
|
|
22
|
+
"@vincemakes/kiso-core": "0.15.7",
|
|
23
|
+
"@vincemakes/kiso-evals": "0.15.7",
|
|
24
|
+
"@vincemakes/kiso-mcp-ext": "0.15.7",
|
|
25
|
+
"@vincemakes/kiso-provider-anthropic": "0.15.7",
|
|
26
|
+
"@vincemakes/kiso-provider-openai": "0.15.7",
|
|
27
|
+
"@vincemakes/kiso-runtime": "0.15.7",
|
|
28
|
+
"@vincemakes/kiso-skills-ext": "0.15.7",
|
|
29
|
+
"@vincemakes/kiso-subagent-ext": "0.15.7",
|
|
30
|
+
"@vincemakes/kiso-task-ext": "0.15.7",
|
|
31
|
+
"@vincemakes/kiso-tools-node": "0.15.7",
|
|
32
|
+
"@vincemakes/kiso-tui": "0.15.7",
|
|
33
|
+
"@vincemakes/kiso-tui-cells": "0.15.7"
|
|
34
34
|
},
|
|
35
35
|
"devDependencies": {
|
|
36
36
|
"@types/node": "^26.1.2",
|