@quadient/skillshare 1.10.1 → 1.10.2
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 +3 -1
- package/dist/agents-claude.js +20 -5
- package/dist/agents-codex.js +9 -2
- package/dist/agents.js +59 -7
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -30,7 +30,9 @@ without permission prompts.
|
|
|
30
30
|
|
|
31
31
|
Codex runs as `codex app-server` and Claude Code with stream-json input and output. The agent
|
|
32
32
|
keeps running between your messages and stops after 15 quiet minutes; the next message resumes
|
|
33
|
-
the same Codex thread or Claude Code session. Sessions need CLI 1.10.0 or newer.
|
|
33
|
+
the same Codex thread or Claude Code session. Sessions need CLI 1.10.0 or newer. Images
|
|
34
|
+
attached to messages reach the agent from CLI 1.10.2: the listener downloads them to a temp
|
|
35
|
+
directory that it removes when the session closes.
|
|
34
36
|
|
|
35
37
|
```sh
|
|
36
38
|
skillshare agents status
|
package/dist/agents-claude.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
2
3
|
import { mkdtemp, rm, writeFile } from 'node:fs/promises';
|
|
3
4
|
import { tmpdir } from 'node:os';
|
|
4
5
|
import { join } from 'node:path';
|
|
@@ -9,6 +10,20 @@ import { CLAUDE_SANDBOX_TOOLS, DEFAULT, SANDBOX_NOTE, clip, isSandboxed, mcpServ
|
|
|
9
10
|
* user's own claude install, so their login and settings apply. MCP config and the session's instructions are passed per
|
|
10
11
|
* process; tokens only through the environment.
|
|
11
12
|
*/
|
|
13
|
+
/** Plain text, or with images the content blocks of the message: its text, then each downloaded image inline as base64. */
|
|
14
|
+
export function claudeContent(message) {
|
|
15
|
+
const images = (message.images ?? []).flatMap(image => {
|
|
16
|
+
try {
|
|
17
|
+
return [{ type: 'image', source: { type: 'base64', media_type: image.mediaType, data: readFileSync(image.path).toString('base64') } }];
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return [];
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
if (images.length === 0)
|
|
24
|
+
return message.text;
|
|
25
|
+
return [...(message.text ? [{ type: 'text', text: message.text }] : []), ...images];
|
|
26
|
+
}
|
|
12
27
|
export function claudeArgs(context, mcpConfigPath) {
|
|
13
28
|
const { start } = context;
|
|
14
29
|
const sandbox = isSandboxed(start.configuration, context);
|
|
@@ -130,7 +145,7 @@ class ClaudeDriver {
|
|
|
130
145
|
return;
|
|
131
146
|
}
|
|
132
147
|
this.current = { resolve, interrupted: false };
|
|
133
|
-
if (!this.write(message
|
|
148
|
+
if (!this.write(message)) {
|
|
134
149
|
this.current = null;
|
|
135
150
|
reject(new Error('Claude Code is not accepting input.'));
|
|
136
151
|
}
|
|
@@ -144,7 +159,7 @@ class ClaudeDriver {
|
|
|
144
159
|
if (!this.current)
|
|
145
160
|
return Promise.resolve('notSent');
|
|
146
161
|
return new Promise(resolve => {
|
|
147
|
-
if (!this.write(message
|
|
162
|
+
if (!this.write(message, outcome => {
|
|
148
163
|
if (outcome === 'steered')
|
|
149
164
|
this.submitted.delete(message.id);
|
|
150
165
|
resolve(outcome);
|
|
@@ -153,12 +168,12 @@ class ClaudeDriver {
|
|
|
153
168
|
this.submitted.add(message.id);
|
|
154
169
|
});
|
|
155
170
|
}
|
|
156
|
-
write(
|
|
171
|
+
write(message, steered) {
|
|
157
172
|
const sent = this.process?.write(JSON.stringify({
|
|
158
|
-
type: 'user', message: { role: 'user', content:
|
|
173
|
+
type: 'user', message: { role: 'user', content: claudeContent(message) }, parent_tool_use_id: null, session_id: this.sessionId ?? '',
|
|
159
174
|
}));
|
|
160
175
|
if (sent)
|
|
161
|
-
this.echoes.push({ text, steered });
|
|
176
|
+
this.echoes.push({ text: message.text, steered });
|
|
162
177
|
return Boolean(sent);
|
|
163
178
|
}
|
|
164
179
|
replayed(content) {
|
package/dist/agents-codex.js
CHANGED
|
@@ -13,6 +13,13 @@ export function codexConfigArgs(args) {
|
|
|
13
13
|
return kept;
|
|
14
14
|
}
|
|
15
15
|
/** thread/start and thread/resume share these: where to work, what may run, the session's MCP servers and instructions. */
|
|
16
|
+
/** The message as Codex user input: its text, then each downloaded image as a local file Codex reads itself. */
|
|
17
|
+
export function codexInput(message) {
|
|
18
|
+
return [
|
|
19
|
+
...(message.text ? [{ type: 'text', text: message.text }] : []),
|
|
20
|
+
...(message.images ?? []).filter(image => image.path).map(image => ({ type: 'localImage', path: image.path })),
|
|
21
|
+
];
|
|
22
|
+
}
|
|
16
23
|
export function codexThreadParams(context) {
|
|
17
24
|
const { start } = context;
|
|
18
25
|
const sandbox = isSandboxed(start.configuration, context);
|
|
@@ -128,7 +135,7 @@ class CodexDriver {
|
|
|
128
135
|
return new Promise((resolve, reject) => {
|
|
129
136
|
this.current = { resolve, usage: { input: 0, cached: 0, cacheWrite: 0, output: 0 } };
|
|
130
137
|
this.request('turn/start', {
|
|
131
|
-
threadId: this.threadId, input:
|
|
138
|
+
threadId: this.threadId, input: codexInput(message),
|
|
132
139
|
...(configuration.reasoning !== DEFAULT ? { effort: configuration.reasoning } : {}),
|
|
133
140
|
}).then(result => {
|
|
134
141
|
if (this.current && typeof result?.turn?.id === 'string')
|
|
@@ -149,7 +156,7 @@ class CodexDriver {
|
|
|
149
156
|
if (!turnId || !this.threadId)
|
|
150
157
|
return 'notSent';
|
|
151
158
|
try {
|
|
152
|
-
await this.request('turn/steer', { threadId: this.threadId, expectedTurnId: turnId, input:
|
|
159
|
+
await this.request('turn/steer', { threadId: this.threadId, expectedTurnId: turnId, input: codexInput(message) });
|
|
153
160
|
return 'steered';
|
|
154
161
|
}
|
|
155
162
|
catch {
|
package/dist/agents.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { createHash, randomBytes } from 'node:crypto';
|
|
2
|
-
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
3
|
-
import { hostname, platform } from 'node:os';
|
|
2
|
+
import { mkdir, readFile, rm, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { hostname, platform, tmpdir } from 'node:os';
|
|
4
4
|
import { dirname, join, resolve } from 'node:path';
|
|
5
5
|
import { setTimeout as delay } from 'node:timers/promises';
|
|
6
6
|
import { createClaudeDriver } from './agents-claude.js';
|
|
7
7
|
import { createCodexDriver } from './agents-codex.js';
|
|
8
8
|
import { detectAgents, profilesPath } from './agents-detect.js';
|
|
9
|
-
import { SessionRunner } from './agents-session.js';
|
|
9
|
+
import { SessionRunner, errorMessage, } from './agents-session.js';
|
|
10
10
|
import { readCliVersion } from './help.js';
|
|
11
11
|
const root = '/api/agents';
|
|
12
12
|
const PROFILE_HINT = 'Declare named profiles in ' + profilesPath() + ' to set a working directory or extra arguments.';
|
|
@@ -29,6 +29,41 @@ async function send(client, method, path, body, signal) {
|
|
|
29
29
|
}
|
|
30
30
|
return response.status === 204 ? undefined : await response.json();
|
|
31
31
|
}
|
|
32
|
+
const MAX_IMAGE_BYTES = 6 * 1024 * 1024;
|
|
33
|
+
const IMAGE_ID = /^[0-9a-f]{32}\.(png|jpg|gif|webp)$/;
|
|
34
|
+
/**
|
|
35
|
+
* Downloads the images of each message into the session's directory, so the agent reads them as local files. An image that
|
|
36
|
+
* cannot be fetched is left out and the agent is told, so the turn still runs.
|
|
37
|
+
*/
|
|
38
|
+
export async function downloadImages(client, directory, messages) {
|
|
39
|
+
return Promise.all(messages.map(async (message) => {
|
|
40
|
+
if (!message.images?.length)
|
|
41
|
+
return message;
|
|
42
|
+
const images = [];
|
|
43
|
+
const problems = [];
|
|
44
|
+
for (const image of message.images) {
|
|
45
|
+
try {
|
|
46
|
+
if (!IMAGE_ID.test(image.id) || !image.url.startsWith(`${root}/sessions/`))
|
|
47
|
+
throw new Error('unexpected image link');
|
|
48
|
+
const response = await client.request(image.url, { method: 'GET' });
|
|
49
|
+
if (!response.ok)
|
|
50
|
+
throw new Error(`HTTP ${response.status}`);
|
|
51
|
+
const data = Buffer.from(await response.arrayBuffer());
|
|
52
|
+
if (data.length > MAX_IMAGE_BYTES)
|
|
53
|
+
throw new Error('the image is too large');
|
|
54
|
+
await mkdir(directory, { recursive: true });
|
|
55
|
+
const path = join(directory, image.id);
|
|
56
|
+
await writeFile(path, data);
|
|
57
|
+
images.push({ ...image, path });
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
problems.push(errorMessage(error));
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
const note = problems.length ? `\n\n[${problems.length} attached image(s) could not be downloaded from Skillshare: ${problems.join('; ')}]` : '';
|
|
64
|
+
return { ...message, text: (message.text + note).trim(), images };
|
|
65
|
+
}));
|
|
66
|
+
}
|
|
32
67
|
/** One stable listener id per Skillshare server and account, so the web keeps routes across restarts. */
|
|
33
68
|
export async function loadListenerId(appDirectory, baseUrl, userId) {
|
|
34
69
|
const key = createHash('sha256').update(baseUrl + '\0' + userId).digest('hex').slice(0, 24);
|
|
@@ -117,6 +152,9 @@ export async function listen(options) {
|
|
|
117
152
|
const now = options.now ?? Date.now;
|
|
118
153
|
const log = options.onProgress ?? (() => { });
|
|
119
154
|
const sessions = new Map();
|
|
155
|
+
// Messages reach a session in order even while earlier ones still download their images.
|
|
156
|
+
const deliveries = new Map();
|
|
157
|
+
const imageRoot = options.imageDirectory ?? join(tmpdir(), 'skillshare-agents');
|
|
120
158
|
let detected = await detect();
|
|
121
159
|
let registeredFingerprint = '';
|
|
122
160
|
let detectedAt = now();
|
|
@@ -162,18 +200,32 @@ export async function listen(options) {
|
|
|
162
200
|
});
|
|
163
201
|
sessions.set(command.sessionId, runner);
|
|
164
202
|
log(`${command.start.resumeThreadId ? 'Resumed' : 'Started'} session ${command.sessionId} "${command.start.title}" with ${label(command.start)}.`);
|
|
165
|
-
void runner.done.then(() => {
|
|
166
|
-
if (sessions.get(command.sessionId) === runner)
|
|
203
|
+
void runner.done.then(async () => {
|
|
204
|
+
if (sessions.get(command.sessionId) === runner) {
|
|
167
205
|
sessions.delete(command.sessionId);
|
|
206
|
+
deliveries.delete(command.sessionId);
|
|
207
|
+
await rm(join(imageRoot, command.sessionId), { recursive: true, force: true }).catch(() => { });
|
|
208
|
+
}
|
|
168
209
|
log(`Closed session ${command.sessionId}.`);
|
|
169
210
|
});
|
|
170
|
-
runner
|
|
211
|
+
deliver(runner, command.messages ?? []);
|
|
212
|
+
}
|
|
213
|
+
function deliver(runner, messages) {
|
|
214
|
+
if (messages.length === 0)
|
|
215
|
+
return;
|
|
216
|
+
if (!messages.some(message => message.images?.length) && !deliveries.has(runner.sessionId))
|
|
217
|
+
return runner.send(messages);
|
|
218
|
+
const previous = deliveries.get(runner.sessionId) ?? Promise.resolve();
|
|
219
|
+
const next = previous.then(() => downloadImages(options.client, join(imageRoot, runner.sessionId), messages)).then(ready => runner.send(ready));
|
|
220
|
+
deliveries.set(runner.sessionId, next);
|
|
221
|
+
void next.finally(() => { if (deliveries.get(runner.sessionId) === next)
|
|
222
|
+
deliveries.delete(runner.sessionId); });
|
|
171
223
|
}
|
|
172
224
|
function handle(command) {
|
|
173
225
|
const runner = sessions.get(command.sessionId);
|
|
174
226
|
switch (command.type) {
|
|
175
227
|
case 'start': return start(command);
|
|
176
|
-
case 'send': return runner
|
|
228
|
+
case 'send': return runner && deliver(runner, command.messages ?? []);
|
|
177
229
|
case 'interrupt': return void runner?.interrupt();
|
|
178
230
|
case 'close': return void runner?.close();
|
|
179
231
|
}
|