workar 0.1.0

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 ADDED
@@ -0,0 +1,58 @@
1
+ # workar
2
+
3
+ End-user CLI for the distributed work system.
4
+
5
+ ## Setup
6
+
7
+ ```bash
8
+ npm install
9
+ npm run build
10
+ ```
11
+
12
+ ## Usage
13
+
14
+ ### Register a user
15
+
16
+ ```bash
17
+ workar register --username alice [--server http://localhost:8787]
18
+ ```
19
+
20
+ Writes `~/.workar/config.json` with `username`, `apiKey`, and `serverUrl`.
21
+
22
+ ### Authenticate (get a JWT)
23
+
24
+ ```bash
25
+ workar auth [--username alice] [--api-key <key>]
26
+ ```
27
+
28
+ Writes the JWT to `~/.workar/config.json`.
29
+
30
+ ### Submit work
31
+
32
+ ```bash
33
+ workar submit --type image-gen --wait [--out-dir ./output] -- prompt="a red panda" model=sdxl-lightning
34
+ ```
35
+
36
+ Key-value pairs after `--` are passed as work fields. `--wait` polls for the
37
+ result and saves it as `work-<workId>.<ext>` in `--out-dir` (default: CWD).
38
+
39
+ ### Retrieve a result
40
+
41
+ ```bash
42
+ workar get [--work-id <id>] [--wait] [--out-dir ./output]
43
+ ```
44
+
45
+ ## Config file
46
+
47
+ `~/.workar/config.json` (chmod 600):
48
+
49
+ ```json
50
+ {
51
+ "serverUrl": "https://...",
52
+ "username": "alice",
53
+ "apiKey": "...",
54
+ "jwt": "..."
55
+ }
56
+ ```
57
+
58
+ Flags `--server`, `--username`, `--api-key` override the file values.
package/dist/api.d.ts ADDED
@@ -0,0 +1,21 @@
1
+ export declare class ClientApi {
2
+ private readonly serverUrl;
3
+ private readonly authHeaders;
4
+ constructor(serverUrl: string, authHeaders: Record<string, string>);
5
+ register(username: string): Promise<{
6
+ username: string;
7
+ apiKey: string;
8
+ }>;
9
+ auth(username: string, apiKey: string): Promise<{
10
+ jwt: string;
11
+ }>;
12
+ submitWork(body: Record<string, unknown>): Promise<{
13
+ workId: string;
14
+ }>;
15
+ getWork(poll: boolean, workId?: string): Promise<{
16
+ workId: string;
17
+ contentType: string;
18
+ bytes: Uint8Array;
19
+ isError: boolean;
20
+ }>;
21
+ }
package/dist/api.js ADDED
@@ -0,0 +1,77 @@
1
+ export class ClientApi {
2
+ serverUrl;
3
+ authHeaders;
4
+ constructor(serverUrl, authHeaders) {
5
+ this.serverUrl = serverUrl;
6
+ this.authHeaders = authHeaders;
7
+ }
8
+ async register(username) {
9
+ const res = await fetch(`${this.serverUrl}/api/users`, {
10
+ method: 'POST',
11
+ headers: { 'content-type': 'application/json' },
12
+ body: JSON.stringify({ username }),
13
+ });
14
+ const data = (await res.json());
15
+ if (!res.ok)
16
+ throw new Error(data['error'] ?? `HTTP ${res.status}`);
17
+ return data;
18
+ }
19
+ async auth(username, apiKey) {
20
+ const res = await fetch(`${this.serverUrl}/api/auth`, {
21
+ method: 'POST',
22
+ headers: { 'content-type': 'application/json' },
23
+ body: JSON.stringify({ username, apiKey }),
24
+ });
25
+ const data = (await res.json());
26
+ if (!res.ok)
27
+ throw new Error(data['error'] ?? `HTTP ${res.status}`);
28
+ return data;
29
+ }
30
+ async submitWork(body) {
31
+ const res = await fetch(`${this.serverUrl}/api/work`, {
32
+ method: 'POST',
33
+ headers: { 'content-type': 'application/json', ...this.authHeaders },
34
+ body: JSON.stringify(body),
35
+ });
36
+ const data = (await res.json());
37
+ if (!res.ok)
38
+ throw new Error(data['error'] ?? `HTTP ${res.status}`);
39
+ return data;
40
+ }
41
+ async getWork(poll, workId) {
42
+ const params = new URLSearchParams();
43
+ if (workId)
44
+ params.set('workId', workId);
45
+ const qs = params.toString();
46
+ const url = `${this.serverUrl}/api/work${qs ? `?${qs}` : ''}`;
47
+ const maxWaitMs = 30 * 60 * 1000; // 30 minutes
48
+ const retryIntervalMs = 10 * 1000; // 10 seconds between retries
49
+ const startTime = Date.now();
50
+ let attempt = 0;
51
+ while (true) {
52
+ attempt++;
53
+ const res = await fetch(url, { headers: this.authHeaders });
54
+ if (res.status === 404) {
55
+ if (!poll || Date.now() - startTime >= maxWaitMs) {
56
+ throw new Error('No work result available (404)');
57
+ }
58
+ if (attempt === 1)
59
+ process.stdout.write('Waiting for result');
60
+ process.stdout.write('.');
61
+ await new Promise((resolve) => setTimeout(resolve, retryIntervalMs));
62
+ continue;
63
+ }
64
+ if (attempt > 1)
65
+ process.stdout.write('\n');
66
+ if (!res.ok) {
67
+ const data = (await res.json().catch(() => ({})));
68
+ throw new Error(data['error'] ?? `HTTP ${res.status}`);
69
+ }
70
+ const isError = res.headers.get('x-work-error') === '1';
71
+ const retWorkId = res.headers.get('x-work-id') ?? '';
72
+ const contentType = res.headers.get('content-type') ?? 'application/octet-stream';
73
+ const bytes = new Uint8Array(await res.arrayBuffer());
74
+ return { workId: retWorkId, contentType, bytes, isError };
75
+ }
76
+ }
77
+ }
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,69 @@
1
+ #!/usr/bin/env node
2
+ import { parseArgs } from 'node:util';
3
+ import { cmdRegister } from './commands/register.js';
4
+ import { cmdAuth } from './commands/auth.js';
5
+ import { cmdSubmit } from './commands/submit.js';
6
+ import { cmdGet } from './commands/get.js';
7
+ const [subcommand, ...restArgs] = process.argv.slice(2);
8
+ switch (subcommand) {
9
+ case 'register': {
10
+ const { values } = parseArgs({
11
+ args: restArgs,
12
+ options: {
13
+ username: { type: 'string' },
14
+ server: { type: 'string' },
15
+ },
16
+ });
17
+ await cmdRegister(values);
18
+ break;
19
+ }
20
+ case 'auth': {
21
+ const { values } = parseArgs({
22
+ args: restArgs,
23
+ options: {
24
+ username: { type: 'string' },
25
+ 'api-key': { type: 'string' },
26
+ server: { type: 'string' },
27
+ },
28
+ });
29
+ await cmdAuth(values);
30
+ break;
31
+ }
32
+ case 'submit': {
33
+ const { values, positionals } = parseArgs({
34
+ args: restArgs,
35
+ options: {
36
+ type: { type: 'string' },
37
+ wait: { type: 'boolean', default: false },
38
+ 'out-dir': { type: 'string' },
39
+ server: { type: 'string' },
40
+ 'api-key': { type: 'string' },
41
+ },
42
+ allowPositionals: true,
43
+ });
44
+ await cmdSubmit({
45
+ ...values,
46
+ positionals,
47
+ });
48
+ break;
49
+ }
50
+ case 'get': {
51
+ const { values } = parseArgs({
52
+ args: restArgs,
53
+ options: {
54
+ 'work-id': { type: 'string' },
55
+ wait: { type: 'boolean', default: false },
56
+ 'out-dir': { type: 'string' },
57
+ server: { type: 'string' },
58
+ 'api-key': { type: 'string' },
59
+ },
60
+ });
61
+ await cmdGet(values);
62
+ break;
63
+ }
64
+ default: {
65
+ console.error(`Unknown subcommand: ${subcommand ?? '(none)'}`);
66
+ console.error('Usage: tarsk <register|auth|submit|get> [options]');
67
+ process.exit(1);
68
+ }
69
+ }
@@ -0,0 +1,5 @@
1
+ export declare function cmdAuth(args: {
2
+ username?: string;
3
+ 'api-key'?: string;
4
+ server?: string;
5
+ }): Promise<void>;
@@ -0,0 +1,20 @@
1
+ import { readConfig, writeConfig, getServerUrl } from '../config.js';
2
+ import { ClientApi } from '../api.js';
3
+ export async function cmdAuth(args) {
4
+ const config = await readConfig();
5
+ const serverUrl = getServerUrl(config, args);
6
+ const username = args.username ?? config.username;
7
+ const apiKey = args['api-key'] ?? config.apiKey;
8
+ if (!username) {
9
+ console.error('Error: --username is required (or run `tarsk register` first)');
10
+ process.exit(1);
11
+ }
12
+ if (!apiKey) {
13
+ console.error('Error: --api-key is required (or run `tarsk register` first)');
14
+ process.exit(1);
15
+ }
16
+ const api = new ClientApi(serverUrl, {});
17
+ const result = await api.auth(username, apiKey);
18
+ await writeConfig({ ...config, jwt: result.jwt });
19
+ console.log('Authenticated successfully');
20
+ }
@@ -0,0 +1,7 @@
1
+ export declare function cmdGet(args: {
2
+ 'work-id'?: string;
3
+ wait?: boolean;
4
+ 'out-dir'?: string;
5
+ server?: string;
6
+ 'api-key'?: string;
7
+ }): Promise<void>;
@@ -0,0 +1,16 @@
1
+ import { mkdir } from 'node:fs/promises';
2
+ import { readConfig, getServerUrl, buildAuthHeaders } from '../config.js';
3
+ import { ClientApi } from '../api.js';
4
+ import { saveResult } from '../output.js';
5
+ export async function cmdGet(args) {
6
+ const config = await readConfig();
7
+ const serverUrl = getServerUrl(config, args);
8
+ const authHeaders = buildAuthHeaders(config, { 'api-key': args['api-key'] });
9
+ const api = new ClientApi(serverUrl, authHeaders);
10
+ const outDir = args['out-dir'] ?? process.cwd();
11
+ await mkdir(outDir, { recursive: true });
12
+ const result = await api.getWork(args.wait ?? false, args['work-id']);
13
+ await saveResult(result, outDir);
14
+ if (result.isError)
15
+ process.exit(1);
16
+ }
@@ -0,0 +1,4 @@
1
+ export declare function cmdRegister(args: {
2
+ username?: string;
3
+ server?: string;
4
+ }): Promise<void>;
@@ -0,0 +1,15 @@
1
+ import { readConfig, writeConfig, getServerUrl } from '../config.js';
2
+ import { ClientApi } from '../api.js';
3
+ export async function cmdRegister(args) {
4
+ if (!args.username) {
5
+ console.error('Error: --username is required');
6
+ process.exit(1);
7
+ }
8
+ const config = await readConfig();
9
+ const serverUrl = getServerUrl(config, args);
10
+ const api = new ClientApi(serverUrl, {});
11
+ const result = await api.register(args.username);
12
+ await writeConfig({ ...config, serverUrl, username: result.username, apiKey: result.apiKey });
13
+ console.log(`Registered as ${result.username}`);
14
+ console.log(`API key: ${result.apiKey}`);
15
+ }
@@ -0,0 +1,8 @@
1
+ export declare function cmdSubmit(args: {
2
+ type?: string;
3
+ wait?: boolean;
4
+ 'out-dir'?: string;
5
+ server?: string;
6
+ 'api-key'?: string;
7
+ positionals?: string[];
8
+ }): Promise<void>;
@@ -0,0 +1,33 @@
1
+ import { mkdir } from 'node:fs/promises';
2
+ import { readConfig, getServerUrl, buildAuthHeaders } from '../config.js';
3
+ import { ClientApi } from '../api.js';
4
+ import { saveResult } from '../output.js';
5
+ export async function cmdSubmit(args) {
6
+ if (!args.type) {
7
+ console.error('Error: --type is required');
8
+ process.exit(1);
9
+ }
10
+ const config = await readConfig();
11
+ const serverUrl = getServerUrl(config, args);
12
+ const authHeaders = buildAuthHeaders(config, { 'api-key': args['api-key'] });
13
+ const api = new ClientApi(serverUrl, authHeaders);
14
+ const kv = {};
15
+ for (const pair of args.positionals ?? []) {
16
+ const eq = pair.indexOf('=');
17
+ if (eq === -1) {
18
+ console.error(`Invalid key=value pair: ${pair}`);
19
+ process.exit(1);
20
+ }
21
+ kv[pair.slice(0, eq)] = pair.slice(eq + 1);
22
+ }
23
+ const { workId } = await api.submitWork({ type: args.type, ...kv });
24
+ console.log(`Submitted work ${workId}`);
25
+ if (args.wait) {
26
+ const outDir = args['out-dir'] ?? process.cwd();
27
+ await mkdir(outDir, { recursive: true });
28
+ const result = await api.getWork(true, workId);
29
+ await saveResult(result, outDir);
30
+ if (result.isError)
31
+ process.exit(1);
32
+ }
33
+ }
@@ -0,0 +1,14 @@
1
+ export interface ClientConfig {
2
+ serverUrl?: string;
3
+ username?: string;
4
+ apiKey?: string;
5
+ jwt?: string;
6
+ }
7
+ export declare function readConfig(): Promise<ClientConfig>;
8
+ export declare function writeConfig(config: ClientConfig): Promise<void>;
9
+ export declare function getServerUrl(config: ClientConfig, overrides: {
10
+ server?: string;
11
+ }): string;
12
+ export declare function buildAuthHeaders(config: ClientConfig, overrides: {
13
+ 'api-key'?: string;
14
+ }): Record<string, string>;
package/dist/config.js ADDED
@@ -0,0 +1,27 @@
1
+ import { readFile, writeFile, mkdir } from 'node:fs/promises';
2
+ import { existsSync } from 'node:fs';
3
+ import { homedir } from 'node:os';
4
+ import path from 'node:path';
5
+ const CONFIG_DIR = path.join(homedir(), '.tarsk-work');
6
+ const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
7
+ export async function readConfig() {
8
+ if (!existsSync(CONFIG_FILE))
9
+ return {};
10
+ const raw = await readFile(CONFIG_FILE, 'utf-8');
11
+ return JSON.parse(raw);
12
+ }
13
+ export async function writeConfig(config) {
14
+ await mkdir(CONFIG_DIR, { recursive: true });
15
+ await writeFile(CONFIG_FILE, JSON.stringify(config, null, 2), { mode: 0o600 });
16
+ }
17
+ export function getServerUrl(config, overrides) {
18
+ return overrides.server ?? config.serverUrl ?? 'https://work.tarsk.io';
19
+ }
20
+ export function buildAuthHeaders(config, overrides) {
21
+ const apiKey = overrides['api-key'] ?? config.apiKey;
22
+ if (apiKey)
23
+ return { 'x-api-key': apiKey };
24
+ if (config.jwt)
25
+ return { authorization: `Bearer ${config.jwt}` };
26
+ throw new Error('No authentication available. Run `tarsk register` then `tarsk auth`.');
27
+ }
@@ -0,0 +1,6 @@
1
+ export declare function saveResult(result: {
2
+ workId: string;
3
+ contentType: string;
4
+ bytes: Uint8Array;
5
+ isError: boolean;
6
+ }, outDir: string): Promise<string>;
package/dist/output.js ADDED
@@ -0,0 +1,25 @@
1
+ import { writeFile } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ const EXT_MAP = {
4
+ 'image/png': 'png',
5
+ 'image/jpeg': 'jpg',
6
+ 'image/webp': 'webp',
7
+ 'application/json': 'json',
8
+ 'text/plain': 'txt',
9
+ };
10
+ function extFor(contentType) {
11
+ const base = contentType.split(';')[0].trim();
12
+ return EXT_MAP[base] ?? 'bin';
13
+ }
14
+ export async function saveResult(result, outDir) {
15
+ const ext = extFor(result.contentType);
16
+ const filename = `work-${result.workId}.${ext}`;
17
+ const filePath = path.join(outDir, filename);
18
+ await writeFile(filePath, result.bytes);
19
+ console.log(`I saved the result to "${filePath}"`);
20
+ if (result.isError) {
21
+ const errData = JSON.parse(new TextDecoder().decode(result.bytes));
22
+ console.error(`Error: ${errData.message ?? 'unknown error'}`);
23
+ }
24
+ return filePath;
25
+ }
package/package.json ADDED
@@ -0,0 +1,19 @@
1
+ {
2
+ "name": "workar",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "bin": {
6
+ "workar": "./dist/cli.js"
7
+ },
8
+ "scripts": {
9
+ "build": "tsc && node -e \"const fs=require('fs');const f='dist/cli.js';fs.writeFileSync(f,'#!/usr/bin/env node\\n'+fs.readFileSync(f,'utf8'))\"",
10
+ "start": "node dist/cli.js"
11
+ },
12
+ "engines": {
13
+ "node": ">=20"
14
+ },
15
+ "devDependencies": {
16
+ "@types/node": "^22.0.0",
17
+ "typescript": "^5.7.0"
18
+ }
19
+ }
package/src/api.ts ADDED
@@ -0,0 +1,87 @@
1
+ export class ClientApi {
2
+ constructor(
3
+ private readonly serverUrl: string,
4
+ private readonly authHeaders: Record<string, string>,
5
+ ) {}
6
+
7
+ async register(username: string): Promise<{ username: string; apiKey: string }> {
8
+ const res = await fetch(`${this.serverUrl}/api/users`, {
9
+ method: 'POST',
10
+ headers: { 'content-type': 'application/json' },
11
+ body: JSON.stringify({ username }),
12
+ });
13
+ const data = (await res.json()) as Record<string, unknown>;
14
+ if (!res.ok) throw new Error((data['error'] as string) ?? `HTTP ${res.status}`);
15
+ return data as { username: string; apiKey: string };
16
+ }
17
+
18
+ async auth(username: string, apiKey: string): Promise<{ jwt: string }> {
19
+ const res = await fetch(`${this.serverUrl}/api/auth`, {
20
+ method: 'POST',
21
+ headers: { 'content-type': 'application/json' },
22
+ body: JSON.stringify({ username, apiKey }),
23
+ });
24
+ const data = (await res.json()) as Record<string, unknown>;
25
+ if (!res.ok) throw new Error((data['error'] as string) ?? `HTTP ${res.status}`);
26
+ return data as { jwt: string };
27
+ }
28
+
29
+ async submitWork(body: Record<string, unknown>): Promise<{ workId: string }> {
30
+ const res = await fetch(`${this.serverUrl}/api/work`, {
31
+ method: 'POST',
32
+ headers: { 'content-type': 'application/json', ...this.authHeaders },
33
+ body: JSON.stringify(body),
34
+ });
35
+ const data = (await res.json()) as Record<string, unknown>;
36
+ if (!res.ok) throw new Error((data['error'] as string) ?? `HTTP ${res.status}`);
37
+ return data as { workId: string };
38
+ }
39
+
40
+ async getWork(
41
+ poll: boolean,
42
+ workId?: string,
43
+ ): Promise<{
44
+ workId: string;
45
+ contentType: string;
46
+ bytes: Uint8Array;
47
+ isError: boolean;
48
+ }> {
49
+ const params = new URLSearchParams();
50
+ if (workId) params.set('workId', workId);
51
+ const qs = params.toString();
52
+ const url = `${this.serverUrl}/api/work${qs ? `?${qs}` : ''}`;
53
+
54
+ const maxWaitMs = 30 * 60 * 1000; // 30 minutes
55
+ const retryIntervalMs = 10 * 1000; // 10 seconds between retries
56
+ const startTime = Date.now();
57
+ let attempt = 0;
58
+
59
+ while (true) {
60
+ attempt++;
61
+ const res = await fetch(url, { headers: this.authHeaders });
62
+
63
+ if (res.status === 404) {
64
+ if (!poll || Date.now() - startTime >= maxWaitMs) {
65
+ throw new Error('No work result available (404)');
66
+ }
67
+ if (attempt === 1) process.stdout.write('Waiting for result');
68
+ process.stdout.write('.');
69
+ await new Promise<void>((resolve) => setTimeout(resolve, retryIntervalMs));
70
+ continue;
71
+ }
72
+
73
+ if (attempt > 1) process.stdout.write('\n');
74
+
75
+ if (!res.ok) {
76
+ const data = (await res.json().catch(() => ({}))) as Record<string, unknown>;
77
+ throw new Error((data['error'] as string) ?? `HTTP ${res.status}`);
78
+ }
79
+
80
+ const isError = res.headers.get('x-work-error') === '1';
81
+ const retWorkId = res.headers.get('x-work-id') ?? '';
82
+ const contentType = res.headers.get('content-type') ?? 'application/octet-stream';
83
+ const bytes = new Uint8Array(await res.arrayBuffer());
84
+ return { workId: retWorkId, contentType, bytes, isError };
85
+ }
86
+ }
87
+ }
package/src/cli.ts ADDED
@@ -0,0 +1,89 @@
1
+ #!/usr/bin/env node
2
+ import { parseArgs } from 'node:util';
3
+ import { cmdRegister } from './commands/register.js';
4
+ import { cmdAuth } from './commands/auth.js';
5
+ import { cmdSubmit } from './commands/submit.js';
6
+ import { cmdGet } from './commands/get.js';
7
+
8
+ const [subcommand, ...restArgs] = process.argv.slice(2);
9
+
10
+ switch (subcommand) {
11
+ case 'register': {
12
+ const { values } = parseArgs({
13
+ args: restArgs,
14
+ options: {
15
+ username: { type: 'string' },
16
+ server: { type: 'string' },
17
+ },
18
+ });
19
+ await cmdRegister(values as { username?: string; server?: string });
20
+ break;
21
+ }
22
+
23
+ case 'auth': {
24
+ const { values } = parseArgs({
25
+ args: restArgs,
26
+ options: {
27
+ username: { type: 'string' },
28
+ 'api-key': { type: 'string' },
29
+ server: { type: 'string' },
30
+ },
31
+ });
32
+ await cmdAuth(values as { username?: string; 'api-key'?: string; server?: string });
33
+ break;
34
+ }
35
+
36
+ case 'submit': {
37
+ const { values, positionals } = parseArgs({
38
+ args: restArgs,
39
+ options: {
40
+ type: { type: 'string' },
41
+ wait: { type: 'boolean', default: false },
42
+ 'out-dir': { type: 'string' },
43
+ server: { type: 'string' },
44
+ 'api-key': { type: 'string' },
45
+ },
46
+ allowPositionals: true,
47
+ });
48
+ await cmdSubmit({
49
+ ...(values as {
50
+ type?: string;
51
+ wait?: boolean;
52
+ 'out-dir'?: string;
53
+ server?: string;
54
+ 'api-key'?: string;
55
+ }),
56
+ positionals,
57
+ });
58
+ break;
59
+ }
60
+
61
+ case 'get': {
62
+ const { values } = parseArgs({
63
+ args: restArgs,
64
+ options: {
65
+ 'work-id': { type: 'string' },
66
+ wait: { type: 'boolean', default: false },
67
+ 'out-dir': { type: 'string' },
68
+ server: { type: 'string' },
69
+ 'api-key': { type: 'string' },
70
+ },
71
+ });
72
+ await cmdGet(
73
+ values as {
74
+ 'work-id'?: string;
75
+ wait?: boolean;
76
+ 'out-dir'?: string;
77
+ server?: string;
78
+ 'api-key'?: string;
79
+ },
80
+ );
81
+ break;
82
+ }
83
+
84
+ default: {
85
+ console.error(`Unknown subcommand: ${subcommand ?? '(none)'}`);
86
+ console.error('Usage: tarsk <register|auth|submit|get> [options]');
87
+ process.exit(1);
88
+ }
89
+ }
@@ -0,0 +1,27 @@
1
+ import { readConfig, writeConfig, getServerUrl } from '../config.js';
2
+ import { ClientApi } from '../api.js';
3
+
4
+ export async function cmdAuth(args: {
5
+ username?: string;
6
+ 'api-key'?: string;
7
+ server?: string;
8
+ }): Promise<void> {
9
+ const config = await readConfig();
10
+ const serverUrl = getServerUrl(config, args);
11
+ const username = args.username ?? config.username;
12
+ const apiKey = args['api-key'] ?? config.apiKey;
13
+
14
+ if (!username) {
15
+ console.error('Error: --username is required (or run `workar register` first)');
16
+ process.exit(1);
17
+ }
18
+ if (!apiKey) {
19
+ console.error('Error: --api-key is required (or run `workar register` first)');
20
+ process.exit(1);
21
+ }
22
+
23
+ const api = new ClientApi(serverUrl, {});
24
+ const result = await api.auth(username, apiKey);
25
+ await writeConfig({ ...config, jwt: result.jwt });
26
+ console.log('Authenticated successfully');
27
+ }
@@ -0,0 +1,24 @@
1
+ import { mkdir } from 'node:fs/promises';
2
+ import { readConfig, getServerUrl, buildAuthHeaders } from '../config.js';
3
+ import { ClientApi } from '../api.js';
4
+ import { saveResult } from '../output.js';
5
+
6
+ export async function cmdGet(args: {
7
+ 'work-id'?: string;
8
+ wait?: boolean;
9
+ 'out-dir'?: string;
10
+ server?: string;
11
+ 'api-key'?: string;
12
+ }): Promise<void> {
13
+ const config = await readConfig();
14
+ const serverUrl = getServerUrl(config, args);
15
+ const authHeaders = buildAuthHeaders(config, { 'api-key': args['api-key'] });
16
+ const api = new ClientApi(serverUrl, authHeaders);
17
+
18
+ const outDir = args['out-dir'] ?? process.cwd();
19
+ await mkdir(outDir, { recursive: true });
20
+
21
+ const result = await api.getWork(args.wait ?? false, args['work-id']);
22
+ await saveResult(result, outDir);
23
+ if (result.isError) process.exit(1);
24
+ }
@@ -0,0 +1,20 @@
1
+ import { readConfig, writeConfig, getServerUrl } from '../config.js';
2
+ import { ClientApi } from '../api.js';
3
+
4
+ export async function cmdRegister(args: {
5
+ username?: string;
6
+ server?: string;
7
+ }): Promise<void> {
8
+ if (!args.username) {
9
+ console.error('Error: --username is required');
10
+ process.exit(1);
11
+ }
12
+
13
+ const config = await readConfig();
14
+ const serverUrl = getServerUrl(config, args);
15
+ const api = new ClientApi(serverUrl, {});
16
+ const result = await api.register(args.username);
17
+ await writeConfig({ ...config, serverUrl, username: result.username, apiKey: result.apiKey });
18
+ console.log(`Registered as ${result.username}`);
19
+ console.log(`API key: ${result.apiKey}`);
20
+ }
@@ -0,0 +1,44 @@
1
+ import { mkdir } from 'node:fs/promises';
2
+ import { readConfig, getServerUrl, buildAuthHeaders } from '../config.js';
3
+ import { ClientApi } from '../api.js';
4
+ import { saveResult } from '../output.js';
5
+
6
+ export async function cmdSubmit(args: {
7
+ type?: string;
8
+ wait?: boolean;
9
+ 'out-dir'?: string;
10
+ server?: string;
11
+ 'api-key'?: string;
12
+ positionals?: string[];
13
+ }): Promise<void> {
14
+ if (!args.type) {
15
+ console.error('Error: --type is required');
16
+ process.exit(1);
17
+ }
18
+
19
+ const config = await readConfig();
20
+ const serverUrl = getServerUrl(config, args);
21
+ const authHeaders = buildAuthHeaders(config, { 'api-key': args['api-key'] });
22
+ const api = new ClientApi(serverUrl, authHeaders);
23
+
24
+ const kv: Record<string, string> = {};
25
+ for (const pair of args.positionals ?? []) {
26
+ const eq = pair.indexOf('=');
27
+ if (eq === -1) {
28
+ console.error(`Invalid key=value pair: ${pair}`);
29
+ process.exit(1);
30
+ }
31
+ kv[pair.slice(0, eq)] = pair.slice(eq + 1);
32
+ }
33
+
34
+ const { workId } = await api.submitWork({ type: args.type, ...kv });
35
+ console.log(`Submitted work ${workId}`);
36
+
37
+ if (args.wait) {
38
+ const outDir = args['out-dir'] ?? process.cwd();
39
+ await mkdir(outDir, { recursive: true });
40
+ const result = await api.getWork(true, workId);
41
+ await saveResult(result, outDir);
42
+ if (result.isError) process.exit(1);
43
+ }
44
+ }
package/src/config.ts ADDED
@@ -0,0 +1,42 @@
1
+ import { readFile, writeFile, mkdir } from 'node:fs/promises';
2
+ import { existsSync } from 'node:fs';
3
+ import { homedir } from 'node:os';
4
+ import path from 'node:path';
5
+
6
+ export interface ClientConfig {
7
+ serverUrl?: string;
8
+ username?: string;
9
+ apiKey?: string;
10
+ jwt?: string;
11
+ }
12
+
13
+ const CONFIG_DIR = path.join(homedir(), '.workar');
14
+ const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
15
+
16
+ export async function readConfig(): Promise<ClientConfig> {
17
+ if (!existsSync(CONFIG_FILE)) return {};
18
+ const raw = await readFile(CONFIG_FILE, 'utf-8');
19
+ return JSON.parse(raw) as ClientConfig;
20
+ }
21
+
22
+ export async function writeConfig(config: ClientConfig): Promise<void> {
23
+ await mkdir(CONFIG_DIR, { recursive: true });
24
+ await writeFile(CONFIG_FILE, JSON.stringify(config, null, 2), { mode: 0o600 });
25
+ }
26
+
27
+ export function getServerUrl(
28
+ config: ClientConfig,
29
+ overrides: { server?: string },
30
+ ): string {
31
+ return overrides.server ?? config.serverUrl ?? 'https://workar.tarsk.io';
32
+ }
33
+
34
+ export function buildAuthHeaders(
35
+ config: ClientConfig,
36
+ overrides: { 'api-key'?: string },
37
+ ): Record<string, string> {
38
+ const apiKey = overrides['api-key'] ?? config.apiKey;
39
+ if (apiKey) return { 'x-api-key': apiKey };
40
+ if (config.jwt) return { authorization: `Bearer ${config.jwt}` };
41
+ throw new Error('No authentication available. Run `workar register` then `workar auth`.');
42
+ }
package/src/output.ts ADDED
@@ -0,0 +1,35 @@
1
+ import { writeFile } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+
4
+ const EXT_MAP: Record<string, string> = {
5
+ 'image/png': 'png',
6
+ 'image/jpeg': 'jpg',
7
+ 'image/webp': 'webp',
8
+ 'application/json': 'json',
9
+ 'text/plain': 'txt',
10
+ };
11
+
12
+ function extFor(contentType: string): string {
13
+ const base = contentType.split(';')[0].trim();
14
+ return EXT_MAP[base] ?? 'bin';
15
+ }
16
+
17
+ export async function saveResult(
18
+ result: { workId: string; contentType: string; bytes: Uint8Array; isError: boolean },
19
+ outDir: string,
20
+ ): Promise<string> {
21
+ const ext = extFor(result.contentType);
22
+ const filename = `work-${result.workId}.${ext}`;
23
+ const filePath = path.join(outDir, filename);
24
+ await writeFile(filePath, result.bytes);
25
+ console.log(`I saved the result to "${filePath}"`);
26
+
27
+ if (result.isError) {
28
+ const errData = JSON.parse(new TextDecoder().decode(result.bytes)) as {
29
+ message?: string;
30
+ };
31
+ console.error(`Error: ${errData.message ?? 'unknown error'}`);
32
+ }
33
+
34
+ return filePath;
35
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,16 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "NodeNext",
5
+ "moduleResolution": "NodeNext",
6
+ "outDir": "dist",
7
+ "rootDir": "src",
8
+ "strict": true,
9
+ "esModuleInterop": true,
10
+ "skipLibCheck": true,
11
+ "declaration": true,
12
+ "types": ["node"]
13
+ },
14
+ "include": ["src/**/*"],
15
+ "exclude": ["node_modules", "dist"]
16
+ }