@shardflux/cli 0.24.0 → 0.25.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/CHANGELOG.md +148 -0
- package/README.md +263 -1
- package/dist/account-args.js +5 -0
- package/dist/account-pool.js +5 -1
- package/dist/acp-journal.js +23 -1
- package/dist/acp-permission-elsewhere.d.ts +38 -0
- package/dist/acp-permission-elsewhere.js +85 -0
- package/dist/acp-rpc.d.ts +2 -1
- package/dist/acp-rpc.js +11 -2
- package/dist/acp.js +95 -22
- package/dist/agent-credentials.d.ts +3 -0
- package/dist/agent-credentials.js +7 -0
- package/dist/args.d.ts +1 -1
- package/dist/args.js +172 -9
- package/dist/board-args.d.ts +6 -0
- package/dist/board-args.js +172 -0
- package/dist/board-commands.d.ts +62 -0
- package/dist/board-commands.js +412 -0
- package/dist/codex-cloud.d.ts +30 -3
- package/dist/codex-cloud.js +86 -12
- package/dist/conversation-commands.d.ts +30 -0
- package/dist/conversation-commands.js +328 -9
- package/dist/conversation-transcript.d.ts +44 -0
- package/dist/conversation-transcript.js +206 -0
- package/dist/conversations.d.ts +7 -3
- package/dist/conversations.js +20 -10
- package/dist/device-approvals.js +4 -3
- package/dist/device-bridge.d.ts +42 -1
- package/dist/device-bridge.js +223 -12
- package/dist/device-folders.d.ts +66 -0
- package/dist/device-folders.js +236 -0
- package/dist/device-helper.d.ts +6 -0
- package/dist/device-helper.js +98 -14
- package/dist/drive-commands.d.ts +25 -0
- package/dist/drive-commands.js +201 -0
- package/dist/errors.js +2 -1
- package/dist/format.js +6 -2
- package/dist/handlers.js +7 -1
- package/dist/land.d.ts +116 -0
- package/dist/land.js +238 -0
- package/dist/main.js +10 -1
- package/dist/move-files.d.ts +39 -0
- package/dist/move-files.js +167 -0
- package/dist/move.js +12 -11
- package/dist/onboarding.d.ts +10 -6
- package/dist/onboarding.js +24 -6
- package/dist/project-git.d.ts +7 -1
- package/dist/project-git.js +16 -3
- package/dist/project-preparation.d.ts +7 -0
- package/dist/project-preparation.js +14 -0
- package/dist/project-template-setup.js +32 -11
- package/dist/router-commands.d.ts +33 -0
- package/dist/router-commands.js +179 -0
- package/dist/task-land-hub.py +150 -0
- package/dist/transcribe-commands.d.ts +7 -0
- package/dist/transcribe-commands.js +80 -0
- package/dist/worktree-twin-engine.d.ts +8 -0
- package/dist/worktree-twin-engine.js +28 -22
- package/package.json +2 -2
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `shard transcribe <file>` (0.25.0+): what a recording says, with each word's time, through @shardflux/sdk's
|
|
3
|
+
* `transcriptions.transcribe()` (GET /v1/transcriptions). The signed-in session or SHARDFLUX_API_KEY, like
|
|
4
|
+
* `shard route` and `shard ask`; the audio goes to the API only and is not kept.
|
|
5
|
+
*/
|
|
6
|
+
import { readFile } from 'node:fs/promises';
|
|
7
|
+
import { extname } from 'node:path';
|
|
8
|
+
import { audioFormatOf } from '@shardflux/sdk';
|
|
9
|
+
import { out, str } from "./commands.js";
|
|
10
|
+
import { EXIT, UsageError } from "./errors.js";
|
|
11
|
+
import { inScope } from "./router-commands.js";
|
|
12
|
+
/** What one transcription holds (the API's bound; past it the rest is not transcribed and `truncated` says so). */
|
|
13
|
+
export const TRANSCRIBE_MAX_BYTES = 6 * 1024 * 1024;
|
|
14
|
+
const FORMATS = ['webm', 'wav', 'pcm16'];
|
|
15
|
+
const BY_EXTENSION = { '.webm': 'webm', '.weba': 'webm', '.wav': 'wav', '.wave': 'wav', '.pcm': 'pcm16', '.raw': 'pcm16' };
|
|
16
|
+
const SAMPLE_RATES = [8000, 16000, 22050, 24000, 32000, 44100, 48000];
|
|
17
|
+
/** `12.34` seconds as `0:12.34`. */
|
|
18
|
+
const clock = (s) => `${Math.floor(s / 60)}:${(s % 60).toFixed(2).padStart(5, '0')}`;
|
|
19
|
+
/** The readable form: the text, then (with --words) one line per word with its start and end. */
|
|
20
|
+
export function transcriptText(t, words) {
|
|
21
|
+
const lines = [t.text || '(no speech)'];
|
|
22
|
+
if (t.truncated)
|
|
23
|
+
lines.push(`(only the first ${Math.round(t.duration)} s were transcribed)`);
|
|
24
|
+
if (words)
|
|
25
|
+
for (const w of t.words)
|
|
26
|
+
lines.push(`${clock(w.start)} ${clock(w.end)} ${w.w}`);
|
|
27
|
+
return `${lines.join('\n')}\n`;
|
|
28
|
+
}
|
|
29
|
+
export const TRANSCRIBE_HANDLERS = {
|
|
30
|
+
async transcribe(ctx, values, [file]) {
|
|
31
|
+
if (!file)
|
|
32
|
+
throw new UsageError('missing <file> for "shard transcribe"');
|
|
33
|
+
const named = str(values.format);
|
|
34
|
+
if (named !== undefined && !FORMATS.includes(named))
|
|
35
|
+
throw new UsageError(`--format must be one of ${FORMATS.join(', ')}`);
|
|
36
|
+
const rateText = str(values['sample-rate']);
|
|
37
|
+
const sampleRate = rateText === undefined ? undefined : Number(rateText);
|
|
38
|
+
if (sampleRate !== undefined && !SAMPLE_RATES.includes(sampleRate))
|
|
39
|
+
throw new UsageError(`--sample-rate must be one of ${SAMPLE_RATES.join(', ')}`);
|
|
40
|
+
const language = str(values.language);
|
|
41
|
+
if (language !== undefined && !/^[a-z]{2,3}$/.test(language))
|
|
42
|
+
throw new UsageError('--language must be an ISO 639-1 code such as en or fi');
|
|
43
|
+
const bytes = await readFile(file).catch((e) => {
|
|
44
|
+
throw new UsageError(`cannot read "${file}" (${e.code === 'EISDIR' ? 'a folder' : e.code ?? e.message})`);
|
|
45
|
+
});
|
|
46
|
+
const format = named ?? audioFormatOf(bytes) ?? BY_EXTENSION[extname(file).toLowerCase()];
|
|
47
|
+
if (!format)
|
|
48
|
+
throw new UsageError(`cannot tell the audio format of "${file}"`, 'pass --format webm|wav|pcm16 (WebM/Opus, PCM WAV or raw 16-bit mono)');
|
|
49
|
+
if (format === 'pcm16' && sampleRate === undefined)
|
|
50
|
+
throw new UsageError('raw pcm16 audio needs --sample-rate (for example 16000)');
|
|
51
|
+
if (format !== 'pcm16' && sampleRate !== undefined)
|
|
52
|
+
throw new UsageError('--sample-rate is only for --format pcm16');
|
|
53
|
+
if (bytes.length > TRANSCRIBE_MAX_BYTES)
|
|
54
|
+
ctx.io.stderr.write(`Only the first ${TRANSCRIBE_MAX_BYTES / 1024 / 1024} MB (and at most 120 s) of "${file}" is transcribed.\n`);
|
|
55
|
+
const audio = bytes.subarray(0, TRANSCRIBE_MAX_BYTES);
|
|
56
|
+
try {
|
|
57
|
+
const transcript = await inScope(ctx, values, (client, scope) => client.transcriptions.transcribe(audio, {
|
|
58
|
+
format,
|
|
59
|
+
...(sampleRate === undefined ? {} : { sampleRate }),
|
|
60
|
+
...(language === undefined ? {} : { language }),
|
|
61
|
+
...scope,
|
|
62
|
+
signal: ctx.signal,
|
|
63
|
+
onEvent: (e) => {
|
|
64
|
+
if (e.type === 'queued')
|
|
65
|
+
ctx.io.stderr.write(`Queued: position ${e.position}, about ${Math.max(1, Math.round(e.expectedWaitMs / 1000))} s\n`);
|
|
66
|
+
},
|
|
67
|
+
}));
|
|
68
|
+
if (ctx.json)
|
|
69
|
+
out(ctx, '', transcript);
|
|
70
|
+
else
|
|
71
|
+
ctx.io.stdout.write(transcriptText(transcript, values.words === true));
|
|
72
|
+
return EXIT.ok;
|
|
73
|
+
}
|
|
74
|
+
catch (err) {
|
|
75
|
+
if (ctx.signal.aborted)
|
|
76
|
+
return EXIT.interrupted;
|
|
77
|
+
throw err;
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
};
|
|
@@ -119,6 +119,14 @@ export interface LoopDeps {
|
|
|
119
119
|
* runs immediately. The first pass that succeeds after failures clears them too and runs again at once.
|
|
120
120
|
*/
|
|
121
121
|
export declare function runTwinLoop(deps: LoopDeps, signal: AbortSignal): Promise<void>;
|
|
122
|
+
/**
|
|
123
|
+
* A VM of the project that reads the project hub: an awake one reads it without waking anything; else the project's
|
|
124
|
+
* small space reader (woken). The twin lands from it when a conversation's VM is gone; `shard task land` merges there.
|
|
125
|
+
*/
|
|
126
|
+
export declare function projectHubCell(ctx: Ctx, projectId: string, signal?: AbortSignal, agentLabel?: string): Promise<{
|
|
127
|
+
cell: TwinCell;
|
|
128
|
+
close(): void;
|
|
129
|
+
}>;
|
|
122
130
|
/** The device helper's twin. Nothing here asks the user anything. */
|
|
123
131
|
export declare function runWorktreeTwin(ctx: Ctx, device: string, signal: AbortSignal): Promise<void>;
|
|
124
132
|
export declare const TWIN_HANDLERS: Record<string, Handler>;
|
|
@@ -500,13 +500,36 @@ function reachable(url) {
|
|
|
500
500
|
});
|
|
501
501
|
}
|
|
502
502
|
const gone = (error) => error.status === 404 || error.details?.reason === 'workspace_deleted';
|
|
503
|
+
const openCell = (ws, agentLabel) => {
|
|
504
|
+
const cell = ws.cell({ agentLabel });
|
|
505
|
+
return { cell, close: () => cell.close() };
|
|
506
|
+
};
|
|
507
|
+
/**
|
|
508
|
+
* A VM of the project that reads the project hub: an awake one reads it without waking anything; else the project's
|
|
509
|
+
* small space reader (woken). The twin lands from it when a conversation's VM is gone; `shard task land` merges there.
|
|
510
|
+
*/
|
|
511
|
+
export async function projectHubCell(ctx, projectId, signal, agentLabel = 'shard-twin') {
|
|
512
|
+
const client = conversationClient(ctx);
|
|
513
|
+
const all = (await listConversations(ctx, projectId)).filter(c => !ended(c))
|
|
514
|
+
.sort((a, b) => (b.last_activity_at ?? '').localeCompare(a.last_activity_at ?? ''));
|
|
515
|
+
for (const c of all) {
|
|
516
|
+
try {
|
|
517
|
+
const ws = await client.workspaces.get(c.workspace_id);
|
|
518
|
+
if (!ws.data.deleted_at && ws.ready)
|
|
519
|
+
return openCell(ws, agentLabel);
|
|
520
|
+
}
|
|
521
|
+
catch { /* another may be awake */ }
|
|
522
|
+
}
|
|
523
|
+
const reader = await client.conversations.spaceReader(projectId, { ...(signal ? { signal } : {}) });
|
|
524
|
+
const ws = await client.workspaces.get(reader.workspace.id);
|
|
525
|
+
if (!ws.ready)
|
|
526
|
+
await ws.wake({ timeoutMs: ctx.wakeTimeoutMs, ...(signal ? { signal } : {}) });
|
|
527
|
+
return openCell(ws, agentLabel);
|
|
528
|
+
}
|
|
503
529
|
/** The device helper's twin. Nothing here asks the user anything. */
|
|
504
530
|
export async function runWorktreeTwin(ctx, device, signal) {
|
|
505
531
|
const client = () => conversationClient(ctx);
|
|
506
|
-
const open = (ws) =>
|
|
507
|
-
const cell = ws.cell({ agentLabel: 'shard-twin' });
|
|
508
|
-
return { cell, close: () => cell.close() };
|
|
509
|
-
};
|
|
532
|
+
const open = (ws) => openCell(ws, 'shard-twin');
|
|
510
533
|
const engine = new TwinEngine({
|
|
511
534
|
records: () => Object.values(localConversations(ctx)),
|
|
512
535
|
conversations: async (projectId) => await listConversations(ctx, projectId),
|
|
@@ -539,24 +562,7 @@ export async function runWorktreeTwin(ctx, device, signal) {
|
|
|
539
562
|
await ws.wake({ timeoutMs: ctx.wakeTimeoutMs, signal });
|
|
540
563
|
return open(ws);
|
|
541
564
|
},
|
|
542
|
-
hubCell:
|
|
543
|
-
// An awake VM of the project reads the hub without waking anything; else the project's small space reader.
|
|
544
|
-
const all = (await listConversations(ctx, projectId)).filter(c => !ended(c))
|
|
545
|
-
.sort((a, b) => (b.last_activity_at ?? '').localeCompare(a.last_activity_at ?? ''));
|
|
546
|
-
for (const c of all) {
|
|
547
|
-
try {
|
|
548
|
-
const ws = await client().workspaces.get(c.workspace_id);
|
|
549
|
-
if (!ws.data.deleted_at && ws.ready)
|
|
550
|
-
return open(ws);
|
|
551
|
-
}
|
|
552
|
-
catch { /* another may be awake */ }
|
|
553
|
-
}
|
|
554
|
-
const reader = await client().conversations.spaceReader(projectId, { signal });
|
|
555
|
-
const ws = await client().workspaces.get(reader.workspace.id);
|
|
556
|
-
if (!ws.ready)
|
|
557
|
-
await ws.wake({ timeoutMs: ctx.wakeTimeoutMs, signal });
|
|
558
|
-
return open(ws);
|
|
559
|
-
},
|
|
565
|
+
hubCell: projectId => projectHubCell(ctx, projectId, signal),
|
|
560
566
|
resume: async (c) => { await client().conversations.resume(c.id, { signal }); },
|
|
561
567
|
land: async (c, action, into) => { await client().conversations.land(c.id, action, { into, signal }); },
|
|
562
568
|
message: async (c, text) => { await client().conversations.send(c.id, text, { signal }); },
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shardflux/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.25.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "shard: the Shardflux command line. Create an account, sign in, set up keys and billing, then open, run commands in, move files to, suspend, resume and fork serverless agent workspaces.",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
"snappyjs": "0.7.0",
|
|
49
49
|
"ws": "8.22.0",
|
|
50
50
|
"yaml": "2.9.1",
|
|
51
|
-
"@shardflux/sdk": "^0.
|
|
51
|
+
"@shardflux/sdk": "^0.28.0"
|
|
52
52
|
},
|
|
53
53
|
"devDependencies": {
|
|
54
54
|
"@eslint/js": "10.0.1",
|