@shardflux/cli 0.14.2 → 0.14.3
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 +5 -0
- package/README.md +1 -0
- package/dist/args.d.ts +1 -1
- package/dist/args.js +1 -1
- package/dist/conversation-stash.d.ts +12 -2
- package/dist/conversation-stash.js +120 -43
- package/dist/device-helper.js +27 -3
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.14.3
|
|
4
|
+
|
|
5
|
+
- Poll device requests and deliver approvals independently of transcript mirroring, so a slow or failed copy cannot delay file-push approvals. Isolate mirror failures by conversation, retry with exponential backoff, and log each failure once per conversation revision with the affected entry name.
|
|
6
|
+
- Skip unknown, nested, symlink and oversized transcript entries while mirroring safe top-level JSONL and JSON files. Log each skipped name once per conversation, preserve the empty append-lock exception, and refuse symlink destinations or ancestors inside the stash.
|
|
7
|
+
|
|
3
8
|
## 0.14.2
|
|
4
9
|
|
|
5
10
|
- `shard agents open` accepts conversation keys, VM keys and conversation ids, reattaching the existing Claude Code or Codex session in its conversation worktree. The menu bar's Open action uses the same path; legacy agents retain their existing behavior.
|
package/README.md
CHANGED
|
@@ -81,6 +81,7 @@ Your branch opens in `.shard/worktrees/<name>`, syncs both ways, and its dev ser
|
|
|
81
81
|
Ctrl-C stops local sync. Your main checkout stays available for your own work.
|
|
82
82
|
Device tools ask Allow once or Always for this project on your Mac before preparing the conversation’s local worktree. Live transcripts stay in
|
|
83
83
|
`~/.config/shardflux/conversations/`; ended sessions enter Claude Code’s or Codex’s native history. Transcript files up to 64 MiB are supported (0.14.0+).
|
|
84
|
+
From 0.14.3, transcript copying runs independently of device approvals. Unknown entries are skipped, and failed copies retry with backoff; diagnostics name the conversation and affected entry.
|
|
84
85
|
|
|
85
86
|
For application workspace commands, `shard signup` creates an account and saves a project key, or set
|
|
86
87
|
`SHARDFLUX_API_KEY` to a key from the console and run `shard login`. `shard --help` lists every command.
|
package/dist/args.d.ts
CHANGED
package/dist/args.js
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
import { parseArgs } from 'node:util';
|
|
13
13
|
import { ACCOUNT_COMMANDS } from "./account-args.js";
|
|
14
14
|
import { UsageError } from "./errors.js";
|
|
15
|
-
export const CLI_VERSION = '0.14.
|
|
15
|
+
export const CLI_VERSION = '0.14.3';
|
|
16
16
|
export const GLOBAL_OPTIONS = {
|
|
17
17
|
'api-url': { type: 'string', value: '<url>', description: 'API base URL (default $SHARDFLUX_API_URL, else https://api.shardflux.dev).' },
|
|
18
18
|
json: { type: 'boolean', description: 'Print machine-readable JSON on stdout (errors as JSON on stderr).' },
|
|
@@ -4,8 +4,18 @@ import type { Conversation, LocalConversation } from './conversations.js';
|
|
|
4
4
|
export declare function rewriteTranscript(bytes: Uint8Array, remoteCwd: string, localCwd: string): Uint8Array;
|
|
5
5
|
export declare function transcriptDestination(c: Conversation, folder: string, filename: string, env: Record<string, string | undefined>): string;
|
|
6
6
|
/** Keep growing copies away from native session discovery until the control plane ends the conversation. */
|
|
7
|
-
export declare function mirrorConversation(ctx: Ctx, record: LocalConversation, source: Workspace): Promise<void>;
|
|
7
|
+
export declare function mirrorConversation(ctx: Ctx, record: LocalConversation, source: Workspace, skipped?: Set<string>): Promise<void>;
|
|
8
8
|
export declare function handoffConversation(ctx: Pick<Ctx, 'io' | 'store' | 'apiUrl' | 'signal'>, c: Conversation, folder: string, dir: string, source?: Pick<Workspace, 'key'> | {
|
|
9
9
|
hub: string;
|
|
10
10
|
}): Promise<void>;
|
|
11
|
-
export declare
|
|
11
|
+
export declare class ConversationMirrorState {
|
|
12
|
+
readonly mirrored: Map<string, string>;
|
|
13
|
+
readonly skipped: Map<string, Set<string>>;
|
|
14
|
+
readonly retries: Map<string, {
|
|
15
|
+
revision: string;
|
|
16
|
+
failures: number;
|
|
17
|
+
retryAt: number;
|
|
18
|
+
reported: Set<string>;
|
|
19
|
+
}>;
|
|
20
|
+
}
|
|
21
|
+
export declare function mirrorProjectConversations(ctx: Ctx, state?: ConversationMirrorState): Promise<void>;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
|
-
import { existsSync, lstatSync, mkdirSync, readFileSync, readdirSync } from 'node:fs';
|
|
2
|
+
import { closeSync, constants, existsSync, fstatSync, lstatSync, mkdirSync, openSync, readFileSync, readdirSync } from 'node:fs';
|
|
3
3
|
import { dirname, join, resolve } from 'node:path';
|
|
4
4
|
import { homedir } from 'node:os';
|
|
5
5
|
import { conversationName, listConversations, localConversations, rememberConversation } from "./conversations.js";
|
|
@@ -45,35 +45,79 @@ function privateDirectory(path) {
|
|
|
45
45
|
at = parent;
|
|
46
46
|
}
|
|
47
47
|
if (existsSync(at) && lstatSync(at).isSymbolicLink())
|
|
48
|
-
throw new Error(
|
|
48
|
+
throw new Error(`Transcript directory must not be a symlink: ${at}`);
|
|
49
49
|
mkdirSync(path, { recursive: true, mode: 0o700 });
|
|
50
50
|
if (lstatSync(path).isSymbolicLink())
|
|
51
|
-
throw new Error(
|
|
51
|
+
throw new Error(`Transcript directory must not be a symlink: ${path}`);
|
|
52
|
+
}
|
|
53
|
+
function readStashFile(file) {
|
|
54
|
+
const fd = openSync(file, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
|
|
55
|
+
try {
|
|
56
|
+
if (!fstatSync(fd).isFile())
|
|
57
|
+
throw new Error(`Stash entry is not a regular file: ${file}`);
|
|
58
|
+
return readFileSync(fd);
|
|
59
|
+
}
|
|
60
|
+
finally {
|
|
61
|
+
closeSync(fd);
|
|
62
|
+
}
|
|
52
63
|
}
|
|
53
64
|
/** Keep growing copies away from native session discovery until the control plane ends the conversation. */
|
|
54
|
-
export async function mirrorConversation(ctx, record, source) {
|
|
65
|
+
export async function mirrorConversation(ctx, record, source, skipped = new Set()) {
|
|
55
66
|
const c = record.conversation;
|
|
56
|
-
|
|
67
|
+
if (!/^[a-zA-Z0-9_-]+$/.test(c.project_id) || !/^conv_[a-z0-9]{26}$/.test(c.id))
|
|
68
|
+
throw new Error('Transcript stash identity is unsafe.');
|
|
69
|
+
let dir = ctx.store.dir;
|
|
70
|
+
// Check each stash component before creating children; no ancestor inside the stash may be a symlink.
|
|
57
71
|
privateDirectory(dir);
|
|
72
|
+
for (const part of ['conversations', c.project_id, c.id]) {
|
|
73
|
+
dir = join(dir, part);
|
|
74
|
+
privateDirectory(dir);
|
|
75
|
+
}
|
|
76
|
+
const root = `/shardflux/project/transcripts/${c.id}`;
|
|
77
|
+
const skip = (name, reason) => {
|
|
78
|
+
if (skipped.has(name))
|
|
79
|
+
return;
|
|
80
|
+
skipped.add(name);
|
|
81
|
+
ctx.io.stderr.write(`Transcript mirror ${c.id}: skipped entry ${JSON.stringify(name)} (${reason}).\n`);
|
|
82
|
+
};
|
|
58
83
|
const cell = source.cell({ wake: null });
|
|
84
|
+
const close = () => cell.close();
|
|
85
|
+
ctx.signal.addEventListener('abort', close, { once: true });
|
|
59
86
|
try {
|
|
87
|
+
ctx.signal.throwIfAborted();
|
|
60
88
|
const manifest = await cell.files.manifest(root, { maxFileBytes: MAX, signal: ctx.signal });
|
|
61
|
-
|
|
62
|
-
|
|
89
|
+
for (const entry of manifest.skipped)
|
|
90
|
+
skip(entry.path, entry.reason);
|
|
63
91
|
let count = 0;
|
|
64
92
|
for (const entry of manifest.entries) {
|
|
65
93
|
// Flush holds this empty regular file while appending the transcript.
|
|
66
94
|
// It is synchronization metadata, not native history to mirror.
|
|
67
95
|
if (entry.path === '.lock' && !entry.symlink && entry.size === 0 && entry.sha256 === 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855')
|
|
68
96
|
continue;
|
|
69
|
-
if (entry.symlink || entry.path.includes('/') || !/^[a-zA-Z0-9_.-]+\.(jsonl|json)$/.test(entry.path))
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
97
|
+
if (entry.symlink || entry.path.includes('..') || entry.path.includes('/') || !/^[a-zA-Z0-9_.-]+\.(jsonl|json)$/.test(entry.path)) {
|
|
98
|
+
skip(entry.path, 'not a safe top-level JSONL or JSON file');
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
if (!Number.isSafeInteger(entry.size) || entry.size < 0 || entry.size > MAX) {
|
|
102
|
+
skip(entry.path, 'outside the 64 MiB mirror bound');
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
try {
|
|
106
|
+
const file = join(dir, entry.path), existing = lstatSync(file, { throwIfNoEntry: false });
|
|
107
|
+
if (existing && !existing.isFile()) {
|
|
108
|
+
skip(entry.path, 'stash destination is not a regular file');
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
const expected = existing ? createHash('sha256').update(readStashFile(file)).digest('hex') : '';
|
|
112
|
+
if (expected !== entry.sha256) {
|
|
113
|
+
const bytes = await cell.files.read(`${root}/${entry.path}`, { length: entry.size + 1 });
|
|
114
|
+
if (bytes.length !== entry.size || createHash('sha256').update(bytes).digest('hex') !== entry.sha256)
|
|
115
|
+
throw new Error('Transcript changed during copying; retaining the previous stash.');
|
|
116
|
+
writePrivateFile(dir, file, new TextDecoder('utf8', { fatal: true }).decode(bytes));
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
catch (e) {
|
|
120
|
+
throw new Error(`Entry ${JSON.stringify(entry.path)}: ${e instanceof Error ? e.message : String(e)}`, { cause: e });
|
|
77
121
|
}
|
|
78
122
|
if (entry.path.endsWith('.jsonl'))
|
|
79
123
|
count++;
|
|
@@ -84,26 +128,27 @@ export async function mirrorConversation(ctx, record, source) {
|
|
|
84
128
|
const binding = await conversationClient(ctx).workspaces.get(c.workspace_id);
|
|
85
129
|
if (!binding.data.deleted_at)
|
|
86
130
|
return;
|
|
87
|
-
const meta = JSON.parse(
|
|
88
|
-
if (!c.harness_session_id || meta.harness !== c.harness || meta.harness_session_id !== c.harness_session_id || !/^[a-zA-Z0-9_.-]+\.jsonl$/.test(meta.file))
|
|
89
|
-
throw new Error('
|
|
90
|
-
const bytes =
|
|
131
|
+
const meta = JSON.parse(readStashFile(join(dir, 'meta.json')).toString('utf8'));
|
|
132
|
+
if (!meta || !c.harness_session_id || meta.harness !== c.harness || meta.harness_session_id !== c.harness_session_id || typeof meta.file !== 'string' || meta.file.includes('..') || !/^[a-zA-Z0-9_.-]+\.jsonl$/.test(meta.file))
|
|
133
|
+
throw new Error('Entry "meta.json": transcript handoff metadata does not match the ended conversation.');
|
|
134
|
+
const bytes = readStashFile(join(dir, meta.file));
|
|
91
135
|
if (bytes.length !== meta.bytes || createHash('sha256').update(bytes).digest('hex') !== meta.sha256)
|
|
92
|
-
throw new Error(
|
|
136
|
+
throw new Error(`Entry ${JSON.stringify(meta.file)}: transcript handoff checkpoint differs from its durable metadata.`);
|
|
93
137
|
await handoffConversation(ctx, { ...c, worktree_path: meta.cwd }, record.folder, dir, source);
|
|
94
138
|
}
|
|
95
139
|
}
|
|
96
140
|
finally {
|
|
141
|
+
ctx.signal.removeEventListener('abort', close);
|
|
97
142
|
cell.close();
|
|
98
143
|
}
|
|
99
144
|
}
|
|
100
145
|
export async function handoffConversation(ctx, c, folder, dir, source) {
|
|
101
146
|
if (c.state !== 'done' && c.state !== 'archived')
|
|
102
147
|
throw new Error('Only an ended conversation can enter native history.');
|
|
103
|
-
const files = readdirSync(dir).filter(f => f.
|
|
148
|
+
const files = readdirSync(dir).filter(f => !f.includes('..') && /^[a-zA-Z0-9_.-]+\.jsonl$/.test(f) && lstatSync(join(dir, f)).isFile());
|
|
104
149
|
if (!files.length)
|
|
105
150
|
throw new Error('Conversation has no transcript to hand off.');
|
|
106
|
-
const writes = files.map(name => ({ dest: transcriptDestination(c, folder, name, ctx.io.env), bytes: rewriteTranscript(
|
|
151
|
+
const writes = files.map(name => ({ dest: transcriptDestination(c, folder, name, ctx.io.env), bytes: rewriteTranscript(readStashFile(join(dir, name)), c.worktree_path, folder) }));
|
|
107
152
|
// Check every destination before the first write. Preserve local native history on any collision.
|
|
108
153
|
for (const { dest, bytes } of writes) {
|
|
109
154
|
if (existsSync(dest) && (lstatSync(dest).isSymbolicLink() || !readFileSync(dest).equals(Buffer.from(bytes))))
|
|
@@ -123,7 +168,12 @@ export async function handoffConversation(ctx, c, folder, dir, source) {
|
|
|
123
168
|
rememberConversation(ctx, c, folder, true);
|
|
124
169
|
writePrivateFile(dir, join(dir, 'handoff.json'), JSON.stringify({ conversation_id: c.id, branch: c.branch, destinations: writes.map(w => w.dest), at: new Date().toISOString() }) + '\n');
|
|
125
170
|
}
|
|
126
|
-
export
|
|
171
|
+
export class ConversationMirrorState {
|
|
172
|
+
mirrored = new Map();
|
|
173
|
+
skipped = new Map();
|
|
174
|
+
retries = new Map();
|
|
175
|
+
}
|
|
176
|
+
export async function mirrorProjectConversations(ctx, state = new ConversationMirrorState()) {
|
|
127
177
|
const records = localConversations(ctx);
|
|
128
178
|
if (!Object.keys(records).length)
|
|
129
179
|
return;
|
|
@@ -133,30 +183,57 @@ export async function mirrorProjectConversations(ctx, mirrored = new Map()) {
|
|
|
133
183
|
if (!c || record.handed_off || !c.harness_session_id)
|
|
134
184
|
continue;
|
|
135
185
|
const revision = JSON.stringify([c.workspace_id, c.harness_session_id, c.state, c.durable, c.last_activity_at, c.summary]);
|
|
136
|
-
if (mirrored.get(c.id) === revision)
|
|
186
|
+
if (state.mirrored.get(c.id) === revision)
|
|
137
187
|
continue;
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
188
|
+
let retry = state.retries.get(c.id);
|
|
189
|
+
if (!retry || retry.revision !== revision) {
|
|
190
|
+
retry = { revision, failures: 0, retryAt: 0, reported: new Set() };
|
|
191
|
+
state.retries.set(c.id, retry);
|
|
192
|
+
}
|
|
193
|
+
if (Date.now() < retry.retryAt)
|
|
194
|
+
continue;
|
|
195
|
+
try {
|
|
196
|
+
rememberConversation(ctx, c, record.folder);
|
|
197
|
+
let source;
|
|
198
|
+
for (const candidate of [c, ...all.filter(other => other.project_id === c.project_id && other.id !== c.id)]) {
|
|
199
|
+
try {
|
|
200
|
+
const ws = await conversationClient(ctx).workspaces.get(candidate.workspace_id);
|
|
201
|
+
if (!ws.data.deleted_at && ws.ready) {
|
|
202
|
+
source = ws;
|
|
203
|
+
break;
|
|
204
|
+
}
|
|
146
205
|
}
|
|
206
|
+
catch { /* Another binding may be available. */ }
|
|
207
|
+
}
|
|
208
|
+
if (!source) {
|
|
209
|
+
const client = conversationClient(ctx);
|
|
210
|
+
const reader = await client.conversations.spaceReader(c.project_id, { signal: ctx.signal });
|
|
211
|
+
source = await client.workspaces.get(reader.workspace.id);
|
|
212
|
+
if (!source.ready)
|
|
213
|
+
await source.wake({ timeoutMs: ctx.wakeTimeoutMs, signal: ctx.signal });
|
|
214
|
+
}
|
|
215
|
+
let skipped = state.skipped.get(c.id);
|
|
216
|
+
if (!skipped) {
|
|
217
|
+
skipped = new Set();
|
|
218
|
+
state.skipped.set(c.id, skipped);
|
|
147
219
|
}
|
|
148
|
-
|
|
220
|
+
await mirrorConversation(ctx, { ...record, conversation: c }, source, skipped);
|
|
221
|
+
retry.failures = 0;
|
|
222
|
+
retry.retryAt = 0;
|
|
223
|
+
// An ended record may precede its VM tombstone; retry until the handoff receipt is written.
|
|
224
|
+
if (c.state !== 'done' && c.state !== 'archived')
|
|
225
|
+
state.mirrored.set(c.id, revision);
|
|
149
226
|
}
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
227
|
+
catch (e) {
|
|
228
|
+
if (ctx.signal.aborted)
|
|
229
|
+
return;
|
|
230
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
231
|
+
const backoff = Math.min(300_000, 5000 * 2 ** Math.min(retry.failures++, 6));
|
|
232
|
+
retry.retryAt = Date.now() + backoff;
|
|
233
|
+
if (!retry.reported.has(message)) {
|
|
234
|
+
retry.reported.add(message);
|
|
235
|
+
ctx.io.stderr.write(`Transcript mirror ${c.id}: ${message} (retrying in ${backoff / 1000}s).\n`);
|
|
236
|
+
}
|
|
156
237
|
}
|
|
157
|
-
await mirrorConversation(ctx, { ...record, conversation: c }, source);
|
|
158
|
-
// An ended record may precede its VM tombstone; retry until the handoff receipt is written.
|
|
159
|
-
if (c.state !== 'done' && c.state !== 'archived')
|
|
160
|
-
mirrored.set(c.id, revision);
|
|
161
238
|
}
|
|
162
239
|
}
|
package/dist/device-helper.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { setTimeout as delay } from 'node:timers/promises';
|
|
2
2
|
import { DeviceExecutor, askConnectedAcp } from "./device-bridge.js";
|
|
3
|
-
import { mirrorProjectConversations } from "./conversation-stash.js";
|
|
3
|
+
import { ConversationMirrorState, mirrorProjectConversations } from "./conversation-stash.js";
|
|
4
4
|
import { withTargetLock } from "./target-lock.js";
|
|
5
5
|
import { join } from 'node:path';
|
|
6
6
|
import { conversationsUnavailable } from "./conversations.js";
|
|
@@ -49,7 +49,7 @@ export async function runDeviceHelper(ctx) {
|
|
|
49
49
|
});
|
|
50
50
|
};
|
|
51
51
|
const working = new Map();
|
|
52
|
-
const mirrored = new
|
|
52
|
+
const mirrored = new ConversationMirrorState();
|
|
53
53
|
const deviceFile = join(ctx.store.dir, 'device-id');
|
|
54
54
|
let deviceId;
|
|
55
55
|
try {
|
|
@@ -61,8 +61,32 @@ export async function runDeviceHelper(ctx) {
|
|
|
61
61
|
}
|
|
62
62
|
const executor = new DeviceExecutor(ctx, ask, deviceId);
|
|
63
63
|
const report = (e) => { ctx.io.stderr.write(`Device bridge: ${e instanceof Error ? e.message : String(e)}\n`); };
|
|
64
|
+
let mirroring;
|
|
64
65
|
try {
|
|
65
66
|
return await withTargetLock(join(ctx.store.dir, 'device-helper-lock'), async () => {
|
|
67
|
+
// A slow or failed transcript copy must not delay heartbeats, request polling or approvals.
|
|
68
|
+
mirroring = (async () => {
|
|
69
|
+
let failures = 0;
|
|
70
|
+
const reported = new Set();
|
|
71
|
+
while (!signal.aborted) {
|
|
72
|
+
let wait = 1000;
|
|
73
|
+
try {
|
|
74
|
+
await mirrorProjectConversations(ctx, mirrored);
|
|
75
|
+
failures = 0;
|
|
76
|
+
}
|
|
77
|
+
catch (e) {
|
|
78
|
+
if (signal.aborted)
|
|
79
|
+
break;
|
|
80
|
+
wait = Math.min(300_000, 5000 * 2 ** Math.min(failures++, 6));
|
|
81
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
82
|
+
if (!conversationsUnavailable(e) && !reported.has(message)) {
|
|
83
|
+
reported.add(message);
|
|
84
|
+
ctx.io.stderr.write(`Transcript mirror discovery: ${message} (retrying in ${wait / 1000}s).\n`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
await delay(wait, undefined, { signal }).catch(() => undefined);
|
|
88
|
+
}
|
|
89
|
+
})();
|
|
66
90
|
while (!signal.aborted) {
|
|
67
91
|
try {
|
|
68
92
|
for (const projectId of new Set(Object.values(localConversations(ctx)).map(r => r.project_id))) {
|
|
@@ -80,7 +104,6 @@ export async function runDeviceHelper(ctx) {
|
|
|
80
104
|
} while (cursor && !signal.aborted);
|
|
81
105
|
}
|
|
82
106
|
await executor.reap();
|
|
83
|
-
await mirrorProjectConversations(ctx, mirrored);
|
|
84
107
|
}
|
|
85
108
|
catch (e) {
|
|
86
109
|
if (!signal.aborted && !conversationsUnavailable(e))
|
|
@@ -99,6 +122,7 @@ export async function runDeviceHelper(ctx) {
|
|
|
99
122
|
pending.clear();
|
|
100
123
|
await executor.close();
|
|
101
124
|
await Promise.all([...working.values()]);
|
|
125
|
+
await mirroring;
|
|
102
126
|
await input.catch(() => undefined);
|
|
103
127
|
}
|
|
104
128
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shardflux/cli",
|
|
3
|
-
"version": "0.14.
|
|
3
|
+
"version": "0.14.3",
|
|
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",
|