@quadient/skillshare 1.9.0 → 1.10.1
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 +14 -7
- package/dist/agents-claude.js +322 -0
- package/dist/agents-codex.js +292 -0
- package/dist/agents-detect.js +73 -12
- package/dist/agents-process.js +50 -0
- package/dist/agents-session.js +296 -0
- package/dist/agents.js +64 -52
- package/dist/help.js +5 -5
- package/dist/index.js +3 -2
- package/dist/teams.js +39 -14
- package/package.json +1 -1
- package/dist/agents-run.js +0 -174
package/dist/help.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { readFileSync } from 'node:fs';
|
|
2
2
|
export const GLOBAL_FLAGS = ['token', 'server', 'help'];
|
|
3
|
-
export const BOOLEAN_FLAGS = new Set(['dry-run', 'force', 'headless', 'help', 'json', 'sandbox']);
|
|
3
|
+
export const BOOLEAN_FLAGS = new Set(['dry-run', 'force', 'headless', 'help', 'json', 'no-teams-watch', 'sandbox']);
|
|
4
4
|
export const COMMANDS = [
|
|
5
5
|
{
|
|
6
6
|
name: 'login',
|
|
@@ -110,14 +110,14 @@ export const COMMANDS = [
|
|
|
110
110
|
},
|
|
111
111
|
{
|
|
112
112
|
name: 'agents',
|
|
113
|
-
summary: 'Run Claude Code or Codex
|
|
113
|
+
summary: 'Run Claude Code or Codex sessions from Skillshare on this computer',
|
|
114
114
|
usage: [
|
|
115
|
-
'agents listen [local] [--name <name>] [--workdir <directory>] [--max-concurrency <1-4>] [--sandbox]',
|
|
115
|
+
'agents listen [local] [--name <name>] [--workdir <directory>] [--max-concurrency <1-4>] [--sandbox] [--no-teams-watch]',
|
|
116
116
|
'agents status [local] [--json]',
|
|
117
117
|
],
|
|
118
|
-
description: 'A listener reports the installed agents, their models and reasoning levels, and the local profiles from ~/.config/skillshare/agents.json.
|
|
118
|
+
description: 'A listener reports the installed agents, their models and reasoning levels, and the local profiles from ~/.config/skillshare/agents.json. It runs Claude Code and Codex sessions that you start and continue under Sessions in Skillshare; under Connected agents you choose which listener and configuration runs Teams 👀 sessions. Agents use your own login and run with your full local permissions, without permission prompts, unless --sandbox (or "sandbox": true in a profile) keeps them read-only. An idle agent is stopped after 15 minutes and resumed by the next message. With a Teams connection, the listener also keeps the Teams session renewed through local browser SSO, as skillshare teams watch does; --no-teams-watch turns that off.',
|
|
119
119
|
examples: ['skillshare agents status', 'skillshare agents listen --workdir ~/Research'],
|
|
120
|
-
flags: ['name', 'workdir', 'max-concurrency', 'sandbox', 'json'],
|
|
120
|
+
flags: ['name', 'workdir', 'max-concurrency', 'sandbox', 'no-teams-watch', 'browser', 'headless', 'browser-profile', 'json'],
|
|
121
121
|
},
|
|
122
122
|
{
|
|
123
123
|
name: 'git',
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { assertCredentialDestination, resolveTeamsObjectId, runTeamsCommand } from './teams.js';
|
|
1
|
+
import { assertCredentialDestination, resolveTeamsObjectId, runTeamsCommand, watchTeamsInBackground } from './teams.js';
|
|
2
2
|
import { runAgentsCommand } from './agents.js';
|
|
3
3
|
import { formatUnknownCommand, parseCliArgs } from './cli-args.js';
|
|
4
4
|
import { findCommand, readCliVersion, renderCommandHelp, renderUsage } from './help.js';
|
|
@@ -149,7 +149,8 @@ async function main() {
|
|
|
149
149
|
assertCredentialDestination(baseUrl);
|
|
150
150
|
if (!options.token && !await loadSession(baseUrl))
|
|
151
151
|
await login(baseUrl);
|
|
152
|
-
|
|
152
|
+
const agentsClient = await createClient(baseUrl, options.token);
|
|
153
|
+
await runAgentsCommand(agentsClient, options.positionals, options.flags, APP_DIR, (signal, log) => watchTeamsInBackground(agentsClient, options.flags, APP_DIR, signal, log));
|
|
153
154
|
return;
|
|
154
155
|
}
|
|
155
156
|
case 'git': {
|
package/dist/teams.js
CHANGED
|
@@ -29,6 +29,44 @@ async function expectOk(response) {
|
|
|
29
29
|
catch { }
|
|
30
30
|
throw new Error(error.error ?? `Skillshare Teams API returned HTTP ${response.status}.`);
|
|
31
31
|
}
|
|
32
|
+
/**
|
|
33
|
+
* Captures a Teams session from the local browser profile and imports it. A watcher renews headless with a deadline and,
|
|
34
|
+
* given the connection it saw, imports conditionally, so a disconnect or another login in the meantime is not overwritten.
|
|
35
|
+
*/
|
|
36
|
+
function teamsConnector(client, flags, appDirectory, user, objectId, watching, signal, log) {
|
|
37
|
+
const key = createHash('sha256').update(client.baseUrl + '\0' + user.id).digest('hex').slice(0, 24);
|
|
38
|
+
const profileDirectory = flags['browser-profile'] ? resolve(flags['browser-profile']) : join(appDirectory, 'teams', key, 'browser-profile');
|
|
39
|
+
return async (connectionId) => {
|
|
40
|
+
const session = await captureTeamsLogin({ profileDirectory, objectId, browser: flags.browser,
|
|
41
|
+
headless: watching ? flags.headless !== 'false' : flags.headless === 'true', recover: watching,
|
|
42
|
+
signal, timeoutMs: watching ? 90_000 : undefined, onProgress: log });
|
|
43
|
+
const headers = { 'Content-Type': 'application/json' };
|
|
44
|
+
if (connectionId)
|
|
45
|
+
headers['If-Match'] = `"${connectionId}"`;
|
|
46
|
+
const response = await client.request(`${root}/session`, { method: 'PUT', headers, body: JSON.stringify(session), signal });
|
|
47
|
+
await expectOk(response);
|
|
48
|
+
await client.post(`${root}/test`);
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Keeps an existing Teams connection renewed while another command runs, exactly like `teams watch`. Returns at once when
|
|
53
|
+
* the account has no Teams connection, so commands can always start it.
|
|
54
|
+
*/
|
|
55
|
+
export async function watchTeamsInBackground(client, flags, appDirectory, signal, log) {
|
|
56
|
+
assertCredentialDestination(client.baseUrl);
|
|
57
|
+
const status = await client.get(`${root}/settings`).catch(() => null);
|
|
58
|
+
if (!status?.connectionId)
|
|
59
|
+
return;
|
|
60
|
+
const user = await client.get('/api/auth/me');
|
|
61
|
+
const objectId = resolveTeamsObjectId(user);
|
|
62
|
+
if (!objectId) {
|
|
63
|
+
log(UNLINKED_ACCOUNT_MESSAGE);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
log('Keeping your Teams session renewed through local browser SSO, as skillshare teams watch does. Use --no-teams-watch to turn this off.');
|
|
67
|
+
await watchTeamsConnection({ readStatus: () => client.get(`${root}/settings`),
|
|
68
|
+
reconnect: teamsConnector(client, flags, appDirectory, user, objectId, true, signal, log), signal, onProgress: log });
|
|
69
|
+
}
|
|
32
70
|
export async function runTeamsCommand(client, args, flags, appDirectory) {
|
|
33
71
|
// Browser credentials may be uploaded only over HTTPS, with HTTP reserved for loopback development.
|
|
34
72
|
assertCredentialDestination(client.baseUrl);
|
|
@@ -38,21 +76,8 @@ export async function runTeamsCommand(client, args, flags, appDirectory) {
|
|
|
38
76
|
const resolvedObjectId = resolveTeamsObjectId(user);
|
|
39
77
|
if (!resolvedObjectId)
|
|
40
78
|
throw new Error(UNLINKED_ACCOUNT_MESSAGE);
|
|
41
|
-
const objectId = resolvedObjectId;
|
|
42
|
-
const key = createHash('sha256').update(client.baseUrl + '\0' + user.id).digest('hex').slice(0, 24);
|
|
43
|
-
const profileDirectory = flags['browser-profile'] ? resolve(flags['browser-profile']) : join(appDirectory, 'teams', key, 'browser-profile');
|
|
44
79
|
const abort = new AbortController();
|
|
45
|
-
|
|
46
|
-
const session = await captureTeamsLogin({ profileDirectory, objectId, browser: flags.browser,
|
|
47
|
-
headless: action === 'watch' ? flags.headless !== 'false' : flags.headless === 'true', recover: action === 'watch',
|
|
48
|
-
signal: abort.signal, timeoutMs: action === 'watch' ? 90_000 : undefined, onProgress: console.log });
|
|
49
|
-
const headers = { 'Content-Type': 'application/json' };
|
|
50
|
-
if (connectionId)
|
|
51
|
-
headers['If-Match'] = `"${connectionId}"`;
|
|
52
|
-
const response = await client.request(`${root}/session`, { method: 'PUT', headers, body: JSON.stringify(session), signal: abort.signal });
|
|
53
|
-
await expectOk(response);
|
|
54
|
-
await client.post(`${root}/test`);
|
|
55
|
-
}
|
|
80
|
+
const connect = teamsConnector(client, flags, appDirectory, user, resolvedObjectId, action === 'watch', abort.signal, console.log);
|
|
56
81
|
if (action === 'login') {
|
|
57
82
|
await connect();
|
|
58
83
|
console.log(`Teams connected. Skillshare refreshes tokens automatically. Use skillshare teams watch for renewed browser SSO after session expiry.`);
|
package/package.json
CHANGED
package/dist/agents-run.js
DELETED
|
@@ -1,174 +0,0 @@
|
|
|
1
|
-
import { spawn } from 'node:child_process';
|
|
2
|
-
import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
|
|
3
|
-
import { tmpdir } from 'node:os';
|
|
4
|
-
import { join } from 'node:path';
|
|
5
|
-
const DEFAULT = 'default';
|
|
6
|
-
/** Read-only tools Claude Code may use in a sandboxed run; MCP servers from the assignment are added per server. */
|
|
7
|
-
export const CLAUDE_SANDBOX_TOOLS = ['Read', 'Grep', 'Glob', 'WebSearch', 'WebFetch'];
|
|
8
|
-
export const SANDBOX_NOTE = '\n\nThis run is sandboxed: you can read files, search and use the provided MCP tools, but you cannot change files or run ' +
|
|
9
|
-
'commands that modify anything. Give your final answer as your last message; writing SKILLSHARE_AGENT_RESULT may not be possible.';
|
|
10
|
-
/**
|
|
11
|
-
* Builds the agent invocation. The prompt goes to stdin and tokens to environment variables, so neither
|
|
12
|
-
* appears in the process list. Agents run without permission prompts, with the user's own configuration.
|
|
13
|
-
*/
|
|
14
|
-
export function isSandboxed(assignment, options) {
|
|
15
|
-
return options.sandbox === true || options.profile?.sandbox === true || assignment.configuration.sandbox === true;
|
|
16
|
-
}
|
|
17
|
-
export function buildAgentCommand(assignment, options) {
|
|
18
|
-
const sandbox = isSandboxed(assignment, options);
|
|
19
|
-
const { configuration } = assignment;
|
|
20
|
-
const env = { SKILLSHARE_AGENT_RESULT: join(options.directory, 'result.md'), SKILLSHARE_TASK_ID: assignment.taskId };
|
|
21
|
-
const servers = assignment.mcpServers.map((server, index) => {
|
|
22
|
-
const variable = `SKILLSHARE_MCP_TOKEN_${index}`;
|
|
23
|
-
env[variable] = server.bearerToken;
|
|
24
|
-
return { name: server.name.replace(/[^A-Za-z0-9_-]/g, '_') || `server_${index}`, url: server.url, variable };
|
|
25
|
-
});
|
|
26
|
-
const cwd = options.profile?.workdir ?? options.workdir;
|
|
27
|
-
const extra = options.profile?.args ?? [];
|
|
28
|
-
if (configuration.agent === 'claude') {
|
|
29
|
-
// A sandboxed run never prompts: anything outside the read-only allowlist is denied.
|
|
30
|
-
const args = ['-p', '--output-format', 'json', ...(sandbox
|
|
31
|
-
? ['--permission-mode', 'dontAsk', '--allowedTools', ...CLAUDE_SANDBOX_TOOLS, ...servers.map(server => `mcp__${server.name}`)]
|
|
32
|
-
: ['--dangerously-skip-permissions'])];
|
|
33
|
-
if (configuration.model !== DEFAULT)
|
|
34
|
-
args.push('--model', configuration.model);
|
|
35
|
-
if (configuration.reasoning !== DEFAULT)
|
|
36
|
-
args.push('--effort', configuration.reasoning);
|
|
37
|
-
if (servers.length > 0)
|
|
38
|
-
args.push('--mcp-config', join(options.directory, 'mcp.json'));
|
|
39
|
-
return { command: 'claude', args: [...args, ...extra], env, cwd };
|
|
40
|
-
}
|
|
41
|
-
const args = ['exec', '--skip-git-repo-check', ...(sandbox ? ['--sandbox', 'read-only', '-c', 'approval_policy="never"'] : ['--dangerously-bypass-approvals-and-sandbox']),
|
|
42
|
-
'--output-last-message', join(options.directory, 'last-message.md')];
|
|
43
|
-
if (configuration.model !== DEFAULT)
|
|
44
|
-
args.push('-m', configuration.model);
|
|
45
|
-
if (configuration.reasoning !== DEFAULT)
|
|
46
|
-
args.push('-c', `model_reasoning_effort=${JSON.stringify(configuration.reasoning)}`);
|
|
47
|
-
for (const server of servers)
|
|
48
|
-
args.push('-c', `mcp_servers.${server.name}.url=${JSON.stringify(server.url)}`, '-c', `mcp_servers.${server.name}.bearer_token_env_var=${JSON.stringify(server.variable)}`);
|
|
49
|
-
return { command: 'codex', args: [...args, ...extra, '-'], env, cwd };
|
|
50
|
-
}
|
|
51
|
-
export function claudeMcpConfig(servers) {
|
|
52
|
-
return JSON.stringify({
|
|
53
|
-
mcpServers: Object.fromEntries(servers.map((server, index) => [server.name.replace(/[^A-Za-z0-9_-]/g, '_') || `server_${index}`,
|
|
54
|
-
{ type: 'http', url: server.url, headers: { Authorization: `Bearer \${SKILLSHARE_MCP_TOKEN_${index}}` } }])),
|
|
55
|
-
}, null, 2);
|
|
56
|
-
}
|
|
57
|
-
/** Reads Claude Code's --output-format json summary. */
|
|
58
|
-
export function parseClaudeOutput(stdout) {
|
|
59
|
-
try {
|
|
60
|
-
const data = JSON.parse(stdout.trim().split(/\r?\n/).filter(Boolean).at(-1) ?? '');
|
|
61
|
-
const usage = data.usage ?? {};
|
|
62
|
-
const input = [usage.input_tokens, usage.cache_creation_input_tokens, usage.cache_read_input_tokens]
|
|
63
|
-
.filter((value) => typeof value === 'number');
|
|
64
|
-
return {
|
|
65
|
-
result: typeof data.result === 'string' ? data.result : undefined,
|
|
66
|
-
isError: data.is_error === true,
|
|
67
|
-
stats: {
|
|
68
|
-
durationMs: typeof data.duration_ms === 'number' ? data.duration_ms : undefined,
|
|
69
|
-
turns: typeof data.num_turns === 'number' ? data.num_turns : undefined,
|
|
70
|
-
costUsd: typeof data.total_cost_usd === 'number' ? data.total_cost_usd : undefined,
|
|
71
|
-
inputTokens: input.length > 0 ? input.reduce((a, b) => a + b, 0) : undefined,
|
|
72
|
-
outputTokens: typeof usage.output_tokens === 'number' ? usage.output_tokens : undefined,
|
|
73
|
-
},
|
|
74
|
-
};
|
|
75
|
-
}
|
|
76
|
-
catch {
|
|
77
|
-
return { isError: false, stats: {} };
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
export async function runAgent(assignment, options) {
|
|
81
|
-
const now = options.now ?? Date.now;
|
|
82
|
-
const started = now();
|
|
83
|
-
const directory = await mkdtemp(join(tmpdir(), 'skillshare-agent-'));
|
|
84
|
-
try {
|
|
85
|
-
const command = buildAgentCommand(assignment, { directory, workdir: options.workdir, profile: options.profile, sandbox: options.sandbox });
|
|
86
|
-
if (assignment.configuration.agent === 'claude' && assignment.mcpServers.length > 0)
|
|
87
|
-
await writeFile(join(directory, 'mcp.json'), claudeMcpConfig(assignment.mcpServers), { mode: 0o600 });
|
|
88
|
-
const prompt = assignment.prompt + (isSandboxed(assignment, options) ? SANDBOX_NOTE : '');
|
|
89
|
-
const outcome = await execute(command, prompt, Math.max(1, assignment.timeoutSeconds) * 1000, options.signal, options.spawnAgent ?? spawn);
|
|
90
|
-
const measured = { durationMs: now() - started };
|
|
91
|
-
const claude = assignment.configuration.agent === 'claude' ? parseClaudeOutput(outcome.stdout) : undefined;
|
|
92
|
-
const stats = { ...measured, ...Object.fromEntries(Object.entries(claude?.stats ?? {}).filter(([, value]) => value !== undefined)) };
|
|
93
|
-
if (outcome.cancelled)
|
|
94
|
-
return { ok: false, reason: 'Cancelled on request.', cancelled: true, stats };
|
|
95
|
-
if (outcome.timedOut)
|
|
96
|
-
return { ok: false, reason: `The agent did not finish within ${assignment.timeoutSeconds} seconds.`, cancelled: false, stats };
|
|
97
|
-
if (outcome.spawnError)
|
|
98
|
-
return { ok: false, reason: `Could not start ${command.command}: ${outcome.spawnError}`, cancelled: false, stats };
|
|
99
|
-
const answer = await readAnswer(directory, assignment.configuration.agent, claude?.result);
|
|
100
|
-
if (outcome.code !== 0 || claude?.isError) {
|
|
101
|
-
const detail = (claude?.isError ? claude.result : undefined) ?? lastLines(outcome.stderr) ?? `exit code ${outcome.code}`;
|
|
102
|
-
return { ok: false, reason: `${command.command} failed: ${detail}`.slice(0, 500), cancelled: false, stats };
|
|
103
|
-
}
|
|
104
|
-
if (!answer)
|
|
105
|
-
return { ok: false, reason: 'The agent finished without an answer.', cancelled: false, stats };
|
|
106
|
-
return { ok: true, markdown: answer, stats };
|
|
107
|
-
}
|
|
108
|
-
finally {
|
|
109
|
-
await rm(directory, { recursive: true, force: true });
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
async function readAnswer(directory, agent, claudeResult) {
|
|
113
|
-
for (const file of ['result.md', ...(agent === 'codex' ? ['last-message.md'] : [])]) {
|
|
114
|
-
try {
|
|
115
|
-
const text = (await readFile(join(directory, file), 'utf8')).trim();
|
|
116
|
-
if (text)
|
|
117
|
-
return text;
|
|
118
|
-
}
|
|
119
|
-
catch { }
|
|
120
|
-
}
|
|
121
|
-
return claudeResult?.trim() || undefined;
|
|
122
|
-
}
|
|
123
|
-
function lastLines(text) {
|
|
124
|
-
const lines = text.trim().split(/\r?\n/).filter(line => line.trim()).slice(-3).join(' ').trim();
|
|
125
|
-
return lines ? lines.slice(-300) : undefined;
|
|
126
|
-
}
|
|
127
|
-
function execute(command, input, timeoutMs, signal, spawnAgent) {
|
|
128
|
-
return new Promise(resolvePromise => {
|
|
129
|
-
const posix = process.platform !== 'win32';
|
|
130
|
-
const child = spawnAgent(command.command, command.args, {
|
|
131
|
-
cwd: command.cwd, env: { ...process.env, ...command.env }, stdio: ['pipe', 'pipe', 'pipe'],
|
|
132
|
-
// A separate process group lets cancellation stop the agent's own child processes too.
|
|
133
|
-
detached: posix, windowsHide: true, shell: !posix,
|
|
134
|
-
});
|
|
135
|
-
let stdout = '';
|
|
136
|
-
let stderr = '';
|
|
137
|
-
let cancelled = false;
|
|
138
|
-
let timedOut = false;
|
|
139
|
-
let settled = false;
|
|
140
|
-
const stop = () => {
|
|
141
|
-
const kill = (sig) => {
|
|
142
|
-
try {
|
|
143
|
-
if (posix && child.pid)
|
|
144
|
-
process.kill(-child.pid, sig);
|
|
145
|
-
else
|
|
146
|
-
child.kill(sig);
|
|
147
|
-
}
|
|
148
|
-
catch { }
|
|
149
|
-
};
|
|
150
|
-
kill('SIGTERM');
|
|
151
|
-
setTimeout(() => kill('SIGKILL'), 5000).unref();
|
|
152
|
-
};
|
|
153
|
-
const onAbort = () => { cancelled = true; stop(); };
|
|
154
|
-
const timer = setTimeout(() => { timedOut = true; stop(); }, timeoutMs);
|
|
155
|
-
signal.addEventListener('abort', onAbort, { once: true });
|
|
156
|
-
const finish = (outcome) => {
|
|
157
|
-
if (settled)
|
|
158
|
-
return;
|
|
159
|
-
settled = true;
|
|
160
|
-
clearTimeout(timer);
|
|
161
|
-
signal.removeEventListener('abort', onAbort);
|
|
162
|
-
resolvePromise(outcome);
|
|
163
|
-
};
|
|
164
|
-
child.stdout?.on('data', chunk => { if (stdout.length < 2_000_000)
|
|
165
|
-
stdout += chunk; });
|
|
166
|
-
child.stderr?.on('data', chunk => { stderr = (stderr + chunk).slice(-4000); });
|
|
167
|
-
child.on('error', error => finish({ code: null, stdout, stderr, cancelled, timedOut, spawnError: error.message }));
|
|
168
|
-
child.on('close', code => finish({ code, stdout, stderr, cancelled, timedOut }));
|
|
169
|
-
child.stdin?.on('error', () => { });
|
|
170
|
-
child.stdin?.end(input);
|
|
171
|
-
if (signal.aborted)
|
|
172
|
-
onAbort();
|
|
173
|
-
});
|
|
174
|
-
}
|