@shardflux/cli 0.25.0 → 0.28.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 +130 -1
- package/README.md +145 -34
- package/dist/account-pool.d.ts +11 -1
- package/dist/account-pool.js +48 -0
- package/dist/agent-commands.js +4 -3
- package/dist/agent-models.d.ts +31 -1
- package/dist/agent-models.js +27 -4
- package/dist/args.d.ts +1 -1
- package/dist/args.js +67 -12
- package/dist/board-args.js +8 -3
- package/dist/board-commands.d.ts +7 -3
- package/dist/board-commands.js +22 -12
- package/dist/brain-commands.d.ts +2 -0
- package/dist/brain-commands.js +78 -0
- package/dist/client-settings.d.ts +2 -1
- package/dist/client-settings.js +14 -2
- package/dist/codex-cloud.d.ts +2 -0
- package/dist/codex-cloud.js +14 -11
- package/dist/commands.js +8 -0
- package/dist/conversation-commands.d.ts +5 -1
- package/dist/conversation-commands.js +58 -6
- package/dist/conversation-terminal.js +2 -2
- package/dist/desktop-linux.d.ts +12 -1
- package/dist/desktop-linux.js +47 -18
- package/dist/device-approvals.js +24 -2
- package/dist/device-bridge.d.ts +21 -4
- package/dist/device-bridge.js +173 -7
- package/dist/device-folders.d.ts +6 -1
- package/dist/device-folders.js +6 -2
- package/dist/device-helper.d.ts +8 -0
- package/dist/device-helper.js +71 -12
- package/dist/device-mode.d.ts +44 -0
- package/dist/device-mode.js +119 -0
- package/dist/drive-commands.d.ts +2 -2
- package/dist/drive-commands.js +1 -1
- package/dist/handlers.js +4 -2
- package/dist/land.d.ts +9 -4
- package/dist/land.js +52 -6
- package/dist/mac-app.d.ts +11 -0
- package/dist/mac-app.js +11 -0
- package/dist/main.js +2 -0
- package/dist/menubar.d.ts +18 -13
- package/dist/menubar.js +60 -50
- package/dist/orchestrator-args.d.ts +7 -0
- package/dist/orchestrator-args.js +64 -0
- package/dist/orchestrator-commands.d.ts +43 -0
- package/dist/orchestrator-commands.js +262 -0
- package/dist/person-scope.d.ts +20 -0
- package/dist/person-scope.js +61 -0
- package/dist/project-hub-seed.d.ts +20 -0
- package/dist/project-hub-seed.js +59 -0
- package/dist/registry.d.ts +1 -1
- package/dist/registry.js +2 -1
- package/dist/router-commands.d.ts +12 -21
- package/dist/router-commands.js +5 -59
- package/dist/t3-launch.js +2 -2
- package/dist/task-land-hub.py +16 -1
- package/dist/this-computer.d.ts +4 -2
- package/dist/this-computer.js +20 -2
- package/dist/thread-on-computer.js +2 -2
- package/dist/timing.js +3 -1
- package/dist/transcribe-commands.js +6 -2
- package/dist/update.js +4 -2
- package/dist/wait-status.d.ts +1 -1
- package/dist/wait-status.js +2 -0
- package/dist/worktree-twin-engine.js +1 -1
- package/dist/worktree-twin.d.ts +6 -0
- package/dist/worktree-twin.js +8 -1
- package/package.json +6 -5
package/dist/handlers.js
CHANGED
|
@@ -5,6 +5,7 @@ import { CONVERSATION_HANDLERS } from "./conversation-commands.js";
|
|
|
5
5
|
import { BOARD_HANDLERS } from "./board-commands.js";
|
|
6
6
|
import { TRANSCRIPT_HANDLERS } from "./conversation-transcript.js";
|
|
7
7
|
import { ROUTER_HANDLERS } from "./router-commands.js";
|
|
8
|
+
import { ORCHESTRATOR_HANDLERS } from "./orchestrator-commands.js";
|
|
8
9
|
import { TRANSCRIBE_HANDLERS } from "./transcribe-commands.js";
|
|
9
10
|
/**
|
|
10
11
|
* Every command's handler, keyed by its path ("workspaces open", "auth login"): the project-key commands of
|
|
@@ -25,11 +26,12 @@ import { TWIN_HANDLERS } from "./worktree-twin-engine.js";
|
|
|
25
26
|
import { CODEX_CLOUD_HANDLERS } from "./codex-cloud.js";
|
|
26
27
|
import { THREAD_HANDLERS } from "./thread-on-computer.js";
|
|
27
28
|
import { CLI_PROXY_HANDLERS } from "./cli-proxy.js";
|
|
28
|
-
import { ACCOUNT_POOL_HANDLERS } from "./account-pool.js";
|
|
29
|
+
import { ACCOUNT_POOL_HANDLERS, AGENT_MODELS_HANDLERS } from "./account-pool.js";
|
|
29
30
|
import { MOVE_HANDLERS } from "./move.js";
|
|
30
31
|
import { AGENT_CONFIG_HANDLERS } from "./agent-config-sync.js";
|
|
31
32
|
import { MACHINE_SETUP_HANDLERS } from "./machine-setup.js";
|
|
32
33
|
import { SETTINGS_HANDLERS } from "./client-settings.js";
|
|
33
34
|
import { PROJECT_PREPARATION_HANDLERS } from "./project-preparation.js";
|
|
34
35
|
import { DRIVE_HANDLERS } from "./drive-commands.js";
|
|
35
|
-
|
|
36
|
+
import { BRAIN_HANDLERS } from "./brain-commands.js";
|
|
37
|
+
export const HANDLERS = { ...PROJECT_PREPARATION_HANDLERS, ...PROJECT_LINK_HANDLERS, ...MOVE_HANDLERS, ...DEVICE_HANDLERS, ...CODEX_CLOUD_HANDLERS, ...CLI_PROXY_HANDLERS, ...THREAD_HANDLERS, ...CONVERSATION_HANDLERS, ...ORCHESTRATOR_HANDLERS, ...BOARD_HANDLERS, ...TRANSCRIPT_HANDLERS, ...LOCAL_CONVERSATION_HANDLERS, ...TWIN_HANDLERS, ...CREDENTIAL_HANDLERS, ...AGENT_CREDENTIAL_HANDLERS, ...ACCOUNT_POOL_HANDLERS, ...AGENT_MODELS_HANDLERS, ...KEY_HANDLERS, ...ACCOUNT_HANDLERS, ...AGENT_HANDLERS, ...CENTER_HANDLERS, ...TARGET_HANDLERS, ...AGENT_GIT_HANDLERS, ...MENUBAR_HANDLERS, ...FILESYSTEM_HANDLERS, ...MACHINE_SETUP_HANDLERS, ...SETTINGS_HANDLERS, ...UPDATE_HANDLERS, ...AGENT_CONFIG_HANDLERS, ...ROUTER_HANDLERS, ...TRANSCRIBE_HANDLERS, ...DRIVE_HANDLERS, ...BRAIN_HANDLERS };
|
package/dist/land.d.ts
CHANGED
|
@@ -4,7 +4,7 @@ import type { TwinCell } from './worktree-twin.js';
|
|
|
4
4
|
/** The hub side of a landing, shipped next to this module (src in the monorepo, dist when published). */
|
|
5
5
|
export declare function hubScript(): string;
|
|
6
6
|
export interface HubLanding {
|
|
7
|
-
state: 'landed' | 'conflict' | 'missing' | 'raced' | 'error';
|
|
7
|
+
state: 'landed' | 'conflict' | 'unrelated' | 'missing' | 'raced' | 'error';
|
|
8
8
|
into: string;
|
|
9
9
|
before?: string | null;
|
|
10
10
|
after?: string;
|
|
@@ -61,8 +61,13 @@ export declare function folderBranch(folder: string, fallback: string): Promise<
|
|
|
61
61
|
* edits stay, and the landed commit is checked to be on the branch.
|
|
62
62
|
*/
|
|
63
63
|
export declare function landInFolder(cell: TwinCell, f: FolderInput): Promise<FolderLanding>;
|
|
64
|
-
/** The
|
|
65
|
-
export declare function
|
|
64
|
+
/** The repository a folder holds, as conversations started from it name it (its origin, else the folder itself). */
|
|
65
|
+
export declare function folderRepository(folder: string): string | null;
|
|
66
|
+
/**
|
|
67
|
+
* The person's folder for a project on this computer: a linked folder (shard projects link) or where it was started.
|
|
68
|
+
* With `repository` (a project holds every folder its people linked), only a folder of that repository.
|
|
69
|
+
*/
|
|
70
|
+
export declare function linkedFolder(ctx: Pick<Ctx, 'store' | 'apiUrl'>, projectId: string, conversationId?: string, repository?: string): string | null;
|
|
66
71
|
export declare const displayPath: (path: string) => string;
|
|
67
72
|
export interface LandOutcome {
|
|
68
73
|
landed: true;
|
|
@@ -96,7 +101,7 @@ export interface LandDeps {
|
|
|
96
101
|
cell: TwinCell;
|
|
97
102
|
close(): void;
|
|
98
103
|
}>;
|
|
99
|
-
folder(projectId: string, conversationId?: string): string | null;
|
|
104
|
+
folder(projectId: string, conversationId?: string, repository?: string): string | null;
|
|
100
105
|
device: string;
|
|
101
106
|
stateDir: string;
|
|
102
107
|
hub?: string;
|
package/dist/land.js
CHANGED
|
@@ -21,6 +21,7 @@ import { conversationClient } from "./conversation-commands.js";
|
|
|
21
21
|
import { localConversations } from "./conversations.js";
|
|
22
22
|
import { CodedError } from "./errors.js";
|
|
23
23
|
import { selectedProjects } from "./project-preparation.js";
|
|
24
|
+
import { projectIdentity } from "./project-identity.js";
|
|
24
25
|
import { HUB_PATH, backupRoot, deviceId, git, gitText, inspectWorktree, landingRound, listWorktrees } from "./worktree-twin.js";
|
|
25
26
|
import { projectHubCell } from "./worktree-twin-engine.js";
|
|
26
27
|
let script;
|
|
@@ -84,9 +85,22 @@ export async function landInFolder(cell, f) {
|
|
|
84
85
|
}
|
|
85
86
|
}
|
|
86
87
|
}
|
|
87
|
-
/** The
|
|
88
|
-
export function
|
|
89
|
-
|
|
88
|
+
/** The repository a folder holds, as conversations started from it name it (its origin, else the folder itself). */
|
|
89
|
+
export function folderRepository(folder) {
|
|
90
|
+
try {
|
|
91
|
+
const id = projectIdentity(folder);
|
|
92
|
+
return id.remote ?? `file://${id.folder}`;
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* The person's folder for a project on this computer: a linked folder (shard projects link) or where it was started.
|
|
100
|
+
* With `repository` (a project holds every folder its people linked), only a folder of that repository.
|
|
101
|
+
*/
|
|
102
|
+
export function linkedFolder(ctx, projectId, conversationId, repository) {
|
|
103
|
+
const repo = (folder) => folder && existsSync(join(folder, '.git')) && (!repository || folderRepository(folder) === repository) ? folder : null;
|
|
90
104
|
const records = localConversations(ctx);
|
|
91
105
|
const own = conversationId ? repo(records[conversationId]?.folder) : null;
|
|
92
106
|
if (own)
|
|
@@ -113,7 +127,7 @@ function message(o) {
|
|
|
113
127
|
return hub;
|
|
114
128
|
}
|
|
115
129
|
export function landDeps(ctx) {
|
|
116
|
-
return { client: conversationClient(ctx), hubCell: p => projectHubCell(ctx, p, ctx.signal, 'shard-land'), folder: (p, c) => linkedFolder(ctx, p, c),
|
|
130
|
+
return { client: conversationClient(ctx), hubCell: p => projectHubCell(ctx, p, ctx.signal, 'shard-land'), folder: (p, c, r) => linkedFolder(ctx, p, c, r),
|
|
117
131
|
device: deviceId(ctx.store.dir), stateDir: ctx.store.dir, signal: ctx.signal };
|
|
118
132
|
}
|
|
119
133
|
async function land(d, l, onConflict) {
|
|
@@ -127,6 +141,19 @@ async function land(d, l, onConflict) {
|
|
|
127
141
|
await onConflict(hub);
|
|
128
142
|
throw new CodedError('land_conflict', `Not landed: ${list(hub.conflicts ?? [])} changed on both sides (${hub.stage === 'integrate' ? `${hub.source} and ${l.result}` : `${l.result} and ${hub.into}`}).`, 'its master merges them and asks to land again', { stage: hub.stage, into: hub.into, conflicts: hub.conflicts ?? [] });
|
|
129
143
|
}
|
|
144
|
+
// The hub's default branch is another repository's (a project holds every folder its people linked): a result
|
|
145
|
+
// whose repository has its folder here lands in that folder, and the hub's default branch stays as it is.
|
|
146
|
+
// Awaited here: the computer that reads the hub closes in `finally` (staging 2026-10-17: returned unawaited, the
|
|
147
|
+
// landing was cut off mid-way, "Interrupted.").
|
|
148
|
+
if (hub.state === 'unrelated' && hub.stage === 'default' && l.repository && l.folder && hub.result)
|
|
149
|
+
return await ownFolder(d, vm.cell, l, hub);
|
|
150
|
+
// No history in common (started from another repository than the branch it lands in): it never clears by trying
|
|
151
|
+
// again, so it says what to do instead.
|
|
152
|
+
if (hub.state === 'unrelated') {
|
|
153
|
+
throw new CodedError('land_unrelated', hub.stage === 'integrate'
|
|
154
|
+
? `Not landed: ${hub.source} and ${l.result} have no history in common; they were started from different repositories.`
|
|
155
|
+
: `Not landed: ${l.result} has no history in common with ${hub.into} on the project hub; it was started from another repository than the one ${hub.into} holds.`, 'land it from the folder of the repository it was started from, or start the task again from this project’s own folder', { stage: hub.stage, into: hub.into });
|
|
156
|
+
}
|
|
130
157
|
if (hub.state === 'missing')
|
|
131
158
|
throw new CodedError('land_nothing', `Nothing to land: the project hub has no ${l.result} yet.`, 'land after the work is done', { branch: l.result });
|
|
132
159
|
if (hub.state !== 'landed')
|
|
@@ -149,6 +176,23 @@ async function land(d, l, onConflict) {
|
|
|
149
176
|
vm.close();
|
|
150
177
|
}
|
|
151
178
|
}
|
|
179
|
+
/** A result into the folder of its own repository only (the hub's default branch holds another repository). */
|
|
180
|
+
async function ownFolder(d, cell, l, hub) {
|
|
181
|
+
const folder = l.folder, into = await folderBranch(folder, 'main');
|
|
182
|
+
const contained = l.sources.filter(s => hub.contained?.includes(s.branch)).map(s => s.conversation_id);
|
|
183
|
+
if (!into)
|
|
184
|
+
return { title: l.title, result: { branch: l.result, sha: hub.result }, hub: { into: '', sha: hub.result, method: 'folder' }, contained,
|
|
185
|
+
folder: { path: folder, display: displayPath(folder), into: '', state: 'skipped', reason: 'detached' } };
|
|
186
|
+
const f = await landInFolder(cell, { folder, into, name: l.name, title: l.title, branch: l.result, base: l.base ?? hub.result, device: d.device, stateDir: d.stateDir,
|
|
187
|
+
...(d.hub ? { hub: d.hub } : {}), ...(d.quietMs !== undefined ? { quietMs: d.quietMs } : {}), ...(d.signal ? { signal: d.signal } : {}) });
|
|
188
|
+
if (f.state !== 'landed' && f.state !== 'already') {
|
|
189
|
+
// Nothing else holds it: a folder that could not take it yet is not a landing.
|
|
190
|
+
throw new CodedError('land_failed', f.state === 'conflict'
|
|
191
|
+
? `Not landed yet: ${list(f.conflicts ?? [])} changed in ${displayPath(folder)} too since ${l.result} started.`
|
|
192
|
+
: `Not landed yet: ${displayPath(folder)} is busy (${f.reason ?? 'busy'}).`, f.state === 'conflict' ? 'commit or put aside those changes, and it lands' : 'it lands once the folder is quiet', { into, folder: displayPath(folder) });
|
|
193
|
+
}
|
|
194
|
+
return { title: l.title, result: { branch: l.result, sha: hub.result }, hub: { into, sha: f.sha ?? hub.result, method: 'folder' }, folder: { ...f, display: displayPath(f.path) }, contained };
|
|
195
|
+
}
|
|
152
196
|
/** What a landing did, for people: the line, then what was not recorded. */
|
|
153
197
|
export function landedText(o) {
|
|
154
198
|
return `${o.message}\n${o.recorded ? '' : `Not recorded on the board: ${o.record_error ?? 'unknown'}.\n`}`;
|
|
@@ -180,9 +224,11 @@ export async function landTask(d, taskId, opts = {}) {
|
|
|
180
224
|
if (w.join_mode !== 'reviewer' && (w.changes?.commits ?? 0) > 0 && w.landed?.how !== 'discarded')
|
|
181
225
|
workers.push(w);
|
|
182
226
|
}
|
|
183
|
-
|
|
227
|
+
// The repository the result is of: its workers' (or its master's), so it lands only in a folder of that repository.
|
|
228
|
+
const repository = workers.find(w => w.repo_remote)?.repo_remote ?? (task.master_id ? (await client.conversations.get(task.master_id).catch(() => null))?.repo_remote : undefined) ?? null;
|
|
229
|
+
const folder = opts.path ?? d.folder(task.project_id, undefined, repository ?? undefined);
|
|
184
230
|
const done = await land(d, { projectId: task.project_id, title, name: taskId, result: resultBranch(taskId), sources: workers.map(w => ({ branch: w.branch, conversation_id: w.id })),
|
|
185
|
-
base: workers.find(w => w.base_commit)?.base_commit ?? null, folder }, async (hub) => {
|
|
231
|
+
base: workers.find(w => w.base_commit)?.base_commit ?? null, folder, repository }, async (hub) => {
|
|
186
232
|
const what = hub.stage === 'integrate' ? `${hub.source} into ${resultBranch(taskId)}` : `${resultBranch(taskId)} into ${hub.into}`;
|
|
187
233
|
await client.tasks.steer(taskId, `Land could not merge ${what}: ${list(hub.conflicts ?? [])} changed on both sides. Merge ${hub.into} from the project hub and every worker branch into ${resultBranch(taskId)}, resolve the conflicts so both changes work (ask the person only if the choice is theirs), run the tests, push ${resultBranch(taskId)} to the hub and seal done again.`, signal);
|
|
188
234
|
});
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which macOS app this CLI installs and updates. On (0.27.0+): the one Shardflux app from
|
|
3
|
+
* @shardflux/desktop-darwin-<arch>, which replaces the menu bar app (@shardflux/menubar-darwin) in place. The CLI's
|
|
4
|
+
* optionalDependencies carry the same packages (test/one-app.test.ts keeps the two in step): with the desktop package
|
|
5
|
+
* in the install, `shard`, its refresh and the running menu bar app's self-update replace the menu bar app with it.
|
|
6
|
+
*/
|
|
7
|
+
export declare const ONE_APP_ON_MAC = true;
|
|
8
|
+
/** The npm package that carries the one Shardflux app for this Mac's CPU (it replaces @shardflux/menubar-darwin). */
|
|
9
|
+
export declare const desktopDarwinPackage: (arch?: string) => string;
|
|
10
|
+
/** The npm package that carries the macOS app this CLI installs and updates (see ONE_APP_ON_MAC). */
|
|
11
|
+
export declare const macAppPackage: (arch?: string) => string;
|
package/dist/mac-app.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which macOS app this CLI installs and updates. On (0.27.0+): the one Shardflux app from
|
|
3
|
+
* @shardflux/desktop-darwin-<arch>, which replaces the menu bar app (@shardflux/menubar-darwin) in place. The CLI's
|
|
4
|
+
* optionalDependencies carry the same packages (test/one-app.test.ts keeps the two in step): with the desktop package
|
|
5
|
+
* in the install, `shard`, its refresh and the running menu bar app's self-update replace the menu bar app with it.
|
|
6
|
+
*/
|
|
7
|
+
export const ONE_APP_ON_MAC = true;
|
|
8
|
+
/** The npm package that carries the one Shardflux app for this Mac's CPU (it replaces @shardflux/menubar-darwin). */
|
|
9
|
+
export const desktopDarwinPackage = (arch = process.arch) => `@shardflux/desktop-darwin-${arch === 'x64' ? 'x64' : 'arm64'}`;
|
|
10
|
+
/** The npm package that carries the macOS app this CLI installs and updates (see ONE_APP_ON_MAC). */
|
|
11
|
+
export const macAppPackage = (arch = process.arch) => ONE_APP_ON_MAC ? desktopDarwinPackage(arch) : '@shardflux/menubar-darwin';
|
package/dist/main.js
CHANGED
|
@@ -137,6 +137,8 @@ function hintFor(info, cred, file, command = '') {
|
|
|
137
137
|
const current = typeof info.details?.current_revision === 'string' ? info.details.current_revision : 'unknown';
|
|
138
138
|
return `the file changed since that revision (current revision: ${current}); read it again, then retry with --expected-revision ${current}`;
|
|
139
139
|
}
|
|
140
|
+
if (reason === 'workspace_disk_full' || info.code === 'workspace_disk_full')
|
|
141
|
+
return 'free space with shard files delete <id|key> <path>, or run a cleanup command; retry the write after space is available';
|
|
140
142
|
if (reason === 'edit_not_found')
|
|
141
143
|
return `--old${index} does not occur in the file (it must match exactly, including whitespace)`;
|
|
142
144
|
if (reason === 'edit_ambiguous')
|
package/dist/menubar.d.ts
CHANGED
|
@@ -30,12 +30,6 @@ export declare function compareAppVersions(left: string, right: string): number;
|
|
|
30
30
|
export declare const SELF_UPDATING_APP = "0.3.2";
|
|
31
31
|
/** CFBundleShortVersionString of a Shardflux app from its XML Info.plist, read without any tool (null: not readable). */
|
|
32
32
|
export declare function plistVersion(app: string): string | null;
|
|
33
|
-
/**
|
|
34
|
-
* Whether any of these processes shows a window: the menu, the launcher, Settings, onboarding, an approval. The status
|
|
35
|
-
* item itself (a strip at the top of the screen on the status layer, owned by the app before macOS 26) does not count.
|
|
36
|
-
* Null when the probe could not answer: then nothing may be replaced.
|
|
37
|
-
*/
|
|
38
|
-
export declare function windowsOpen(text: string): boolean | null;
|
|
39
33
|
export declare function parseNativeStatus(text: string): NativeStatus;
|
|
40
34
|
/**
|
|
41
35
|
* The app's executables keep their execute permission (0.13.6+). @shardflux/menubar-darwin 0.1.1 and 0.1.2 reached npm
|
|
@@ -56,17 +50,15 @@ export declare class MenubarManager {
|
|
|
56
50
|
get platform(): NodeJS.Platform;
|
|
57
51
|
/** The version of the app this CLI's npm package carries (what `start` installs). */
|
|
58
52
|
packagedVersion(): Promise<string>;
|
|
59
|
-
/** Whether the app shows any window (see windowsOpen); null when that could not be read. */
|
|
60
|
-
windowsOpen(pids: number[]): Promise<boolean | null>;
|
|
61
53
|
status(configDir?: string): Promise<MenubarStatus>;
|
|
62
54
|
stop(configDir?: string): Promise<MenubarStatus>;
|
|
63
55
|
login(value: 'on' | 'off', configDir?: string): Promise<MenubarStatus>;
|
|
64
56
|
start(configDir: string, apiUrl: string): Promise<MenubarStatus>;
|
|
65
57
|
/**
|
|
66
58
|
* Brings an installed app up to the one this CLI's npm package carries, without disturbing the person: nothing when
|
|
67
|
-
* the versions are equal
|
|
68
|
-
*
|
|
69
|
-
*
|
|
59
|
+
* the versions are equal, the copy on disk alone when it is not running. A running app swaps itself in when it is
|
|
60
|
+
* idle (every app from SELF_UPDATING_APP on: the menu bar app, and the one Shardflux app that replaces it, which runs
|
|
61
|
+
* `shard menubar start` when nothing is open); an older running one is restarted unless a `shard update` runs.
|
|
70
62
|
*/
|
|
71
63
|
refresh(configDir: string, apiUrl: string): Promise<{
|
|
72
64
|
result: 'current' | 'not_installed' | 'installed' | 'restarted' | 'deferred' | 'busy';
|
|
@@ -79,8 +71,21 @@ export declare class MenubarManager {
|
|
|
79
71
|
export declare function ensureMenubar(dir: string, apiUrl: string, manager?: MenubarManager, platform?: NodeJS.Platform): Promise<string | undefined>;
|
|
80
72
|
/** How often a command may start a background refresh while the app is busy (a window open). */
|
|
81
73
|
export declare const REFRESH_RETRY_MS: number;
|
|
82
|
-
/**
|
|
83
|
-
|
|
74
|
+
/**
|
|
75
|
+
* Where this CLI's npm package keeps the app it carries, or null when the optional package is missing. The links npm
|
|
76
|
+
* leaves out of the package are put back first (restorePackagedAppLinks), so the path is the complete, signed app.
|
|
77
|
+
*/
|
|
78
|
+
export declare function packagedAppPath(arch?: string): string | null;
|
|
79
|
+
/**
|
|
80
|
+
* Puts back the symbolic links of the package's Shardflux.app that npm leaves out of a package (0.27.0+): an Electron
|
|
81
|
+
* app's frameworks are made of them (Versions/Current -> A, ...), and without them the app neither starts nor verifies.
|
|
82
|
+
* The package lists them in links.json; its postinstall restores them too, but pnpm and bun skip install scripts. Only
|
|
83
|
+
* relative links inside the app; a path that exists is left as it is. `missing`: links that could not be created.
|
|
84
|
+
*/
|
|
85
|
+
export declare function restorePackagedAppLinks(packageDir: string): {
|
|
86
|
+
restored: string[];
|
|
87
|
+
missing: string[];
|
|
88
|
+
};
|
|
84
89
|
export interface BackgroundRefreshOptions {
|
|
85
90
|
dir: string;
|
|
86
91
|
home: string;
|
package/dist/menubar.js
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
import { desktopAccount } from "./desktop-account.js";
|
|
2
2
|
import { execFile, spawn } from 'node:child_process';
|
|
3
|
-
import { chmodSync, closeSync, cpSync, existsSync, lstatSync, mkdirSync, mkdtempSync, openSync, readdirSync, readFileSync, renameSync, rmSync, writeSync } from 'node:fs';
|
|
3
|
+
import { chmodSync, closeSync, cpSync, existsSync, lstatSync, mkdirSync, mkdtempSync, openSync, readdirSync, readFileSync, renameSync, rmSync, symlinkSync, writeSync } from 'node:fs';
|
|
4
4
|
import { createRequire } from 'node:module';
|
|
5
5
|
import { homedir } from 'node:os';
|
|
6
|
-
import { dirname, join } from 'node:path';
|
|
6
|
+
import { dirname, isAbsolute, join, resolve as resolvePath, sep } from 'node:path';
|
|
7
7
|
import { promisify } from 'node:util';
|
|
8
8
|
import { compareVersions } from '@shardflux/sdk';
|
|
9
9
|
import { AgentStateStore } from "./agent-state.js";
|
|
10
10
|
import { UsageError } from "./errors.js";
|
|
11
11
|
import { json } from "./format.js";
|
|
12
12
|
import { LinuxDesktopManager, linuxDesktopPackage } from "./desktop-linux.js";
|
|
13
|
+
import { macAppPackage } from "./mac-app.js";
|
|
13
14
|
const execute = promisify(execFile);
|
|
14
15
|
/**
|
|
15
16
|
* Every native tool a default MenubarManager runs (plutil, codesign, xattr, open, the app's --control) goes through
|
|
@@ -56,35 +57,6 @@ export function plistVersion(app) {
|
|
|
56
57
|
return null;
|
|
57
58
|
}
|
|
58
59
|
}
|
|
59
|
-
const WINDOW_PROBE = `ObjC.import('CoreGraphics');
|
|
60
|
-
function run(argv) {
|
|
61
|
-
const pids = argv.map(Number);
|
|
62
|
-
const list = ObjC.deepUnwrap(ObjC.castRefToObject($.CGWindowListCopyWindowInfo($.kCGWindowListOptionOnScreenOnly, $.kCGNullWindowID))) || [];
|
|
63
|
-
return JSON.stringify(list.filter(w => pids.includes(w.kCGWindowOwnerPID)).map(w => ({ pid: w.kCGWindowOwnerPID, layer: w.kCGWindowLayer,
|
|
64
|
-
x: w.kCGWindowBounds.X, y: w.kCGWindowBounds.Y, width: w.kCGWindowBounds.Width, height: w.kCGWindowBounds.Height, alpha: w.kCGWindowAlpha })));
|
|
65
|
-
}`;
|
|
66
|
-
/**
|
|
67
|
-
* Whether any of these processes shows a window: the menu, the launcher, Settings, onboarding, an approval. The status
|
|
68
|
-
* item itself (a strip at the top of the screen on the status layer, owned by the app before macOS 26) does not count.
|
|
69
|
-
* Null when the probe could not answer: then nothing may be replaced.
|
|
70
|
-
*/
|
|
71
|
-
export function windowsOpen(text) {
|
|
72
|
-
let rows;
|
|
73
|
-
try {
|
|
74
|
-
rows = JSON.parse(text);
|
|
75
|
-
}
|
|
76
|
-
catch {
|
|
77
|
-
return null;
|
|
78
|
-
}
|
|
79
|
-
if (!Array.isArray(rows))
|
|
80
|
-
return null;
|
|
81
|
-
const windows = rows;
|
|
82
|
-
if (!windows.every(w => w && typeof w === 'object' && ['layer', 'y', 'width', 'height'].every(k => typeof w[k] === 'number')))
|
|
83
|
-
return null;
|
|
84
|
-
// A status item is a strip on the status layer at the top of the screen (39 pt high on a notched display).
|
|
85
|
-
const statusItem = (w) => w.layer === 25 && w.y <= 0 && w.height <= 50;
|
|
86
|
-
return windows.some(w => w.layer >= 0 && !statusItem(w) && (w.alpha ?? 1) > 0 && w.width > 1 && w.height > 1);
|
|
87
|
-
}
|
|
88
60
|
export function parseNativeStatus(text) {
|
|
89
61
|
const value = JSON.parse(text);
|
|
90
62
|
if (!value || typeof value !== 'object')
|
|
@@ -129,6 +101,8 @@ export class MenubarManager {
|
|
|
129
101
|
const app = packagedAppPath();
|
|
130
102
|
if (!app)
|
|
131
103
|
throw new UsageError('The macOS app package is missing. Reinstall shard with optional dependencies enabled, then run shard menubar start.');
|
|
104
|
+
if (restorePackagedAppLinks(dirname(app)).missing.length)
|
|
105
|
+
throw new UsageError(`The Shardflux app in ${dirname(app)} is incomplete and that folder is not writable. Reinstall shard (npm i -g shardflux), then run shard.`);
|
|
132
106
|
return app;
|
|
133
107
|
});
|
|
134
108
|
}
|
|
@@ -150,17 +124,6 @@ export class MenubarManager {
|
|
|
150
124
|
compareBundleVersions(version, version);
|
|
151
125
|
return version;
|
|
152
126
|
}
|
|
153
|
-
/** Whether the app shows any window (see windowsOpen); null when that could not be read. */
|
|
154
|
-
async windowsOpen(pids) {
|
|
155
|
-
if (!pids.length)
|
|
156
|
-
return false;
|
|
157
|
-
try {
|
|
158
|
-
return windowsOpen(await this.#run('/usr/bin/osascript', ['-l', 'JavaScript', '-e', WINDOW_PROBE, ...pids.map(String)]));
|
|
159
|
-
}
|
|
160
|
-
catch {
|
|
161
|
-
return null;
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
127
|
/**
|
|
165
128
|
* The app's --control for `configDir`: its pids are the app running for that config directory, wherever it runs from
|
|
166
129
|
* (menu bar 0.8.4+, one app per config directory), and any app running from the installed bundle.
|
|
@@ -213,7 +176,9 @@ export class MenubarManager {
|
|
|
213
176
|
const staged = join(stage, 'Shardflux.app'), backup = join(stage, 'previous.app');
|
|
214
177
|
let committed = false, restored = false;
|
|
215
178
|
try {
|
|
216
|
-
|
|
179
|
+
// Relative symlinks stay relative (an app's frameworks are made of them): Node's default rewrites them to absolute
|
|
180
|
+
// paths into the npm package, which breaks the bundle's seal and its helpers (0.27.0+).
|
|
181
|
+
cpSync(source, staged, { recursive: true, dereference: false, verbatimSymlinks: true });
|
|
217
182
|
if (current.running)
|
|
218
183
|
await this.stop(configDir);
|
|
219
184
|
if (current.installed)
|
|
@@ -265,9 +230,9 @@ export class MenubarManager {
|
|
|
265
230
|
}
|
|
266
231
|
/**
|
|
267
232
|
* Brings an installed app up to the one this CLI's npm package carries, without disturbing the person: nothing when
|
|
268
|
-
* the versions are equal
|
|
269
|
-
*
|
|
270
|
-
*
|
|
233
|
+
* the versions are equal, the copy on disk alone when it is not running. A running app swaps itself in when it is
|
|
234
|
+
* idle (every app from SELF_UPDATING_APP on: the menu bar app, and the one Shardflux app that replaces it, which runs
|
|
235
|
+
* `shard menubar start` when nothing is open); an older running one is restarted unless a `shard update` runs.
|
|
271
236
|
*/
|
|
272
237
|
async refresh(configDir, apiUrl) {
|
|
273
238
|
this.#requireMac();
|
|
@@ -280,7 +245,7 @@ export class MenubarManager {
|
|
|
280
245
|
if (current.running) {
|
|
281
246
|
if (compareAppVersions(from, SELF_UPDATING_APP) >= 0)
|
|
282
247
|
return { result: 'deferred', from, to };
|
|
283
|
-
if (updateRunning(configDir)
|
|
248
|
+
if (updateRunning(configDir))
|
|
284
249
|
return { result: 'busy', from, to };
|
|
285
250
|
await this.start(configDir, apiUrl);
|
|
286
251
|
return { result: 'restarted', from, to };
|
|
@@ -332,14 +297,59 @@ const REFRESH_MARK = 'menubar-refresh.json';
|
|
|
332
297
|
const REFRESH_LOCK = 'menubar-refresh.lock';
|
|
333
298
|
/** How often a command may start a background refresh while the app is busy (a window open). */
|
|
334
299
|
export const REFRESH_RETRY_MS = 5 * 60_000;
|
|
335
|
-
/**
|
|
336
|
-
|
|
300
|
+
/**
|
|
301
|
+
* Where this CLI's npm package keeps the app it carries, or null when the optional package is missing. The links npm
|
|
302
|
+
* leaves out of the package are put back first (restorePackagedAppLinks), so the path is the complete, signed app.
|
|
303
|
+
*/
|
|
304
|
+
export function packagedAppPath(arch = process.arch) {
|
|
305
|
+
let dir;
|
|
337
306
|
try {
|
|
338
|
-
|
|
307
|
+
dir = dirname(createRequire(import.meta.url).resolve(`${macAppPackage(arch)}/package.json`));
|
|
339
308
|
}
|
|
340
309
|
catch {
|
|
341
310
|
return null;
|
|
342
311
|
}
|
|
312
|
+
restorePackagedAppLinks(dir);
|
|
313
|
+
return join(dir, 'Shardflux.app');
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* Puts back the symbolic links of the package's Shardflux.app that npm leaves out of a package (0.27.0+): an Electron
|
|
317
|
+
* app's frameworks are made of them (Versions/Current -> A, ...), and without them the app neither starts nor verifies.
|
|
318
|
+
* The package lists them in links.json; its postinstall restores them too, but pnpm and bun skip install scripts. Only
|
|
319
|
+
* relative links inside the app; a path that exists is left as it is. `missing`: links that could not be created.
|
|
320
|
+
*/
|
|
321
|
+
export function restorePackagedAppLinks(packageDir) {
|
|
322
|
+
const restored = [], missing = [];
|
|
323
|
+
let spec;
|
|
324
|
+
try {
|
|
325
|
+
spec = JSON.parse(readFileSync(join(packageDir, 'links.json'), 'utf8'));
|
|
326
|
+
}
|
|
327
|
+
catch {
|
|
328
|
+
return { restored, missing };
|
|
329
|
+
}
|
|
330
|
+
const app = join(packageDir, 'Shardflux.app');
|
|
331
|
+
const links = spec.links;
|
|
332
|
+
for (const entry of Array.isArray(links) ? links : []) {
|
|
333
|
+
const [path, target] = Array.isArray(entry) ? entry : [];
|
|
334
|
+
if (typeof path !== 'string' || typeof target !== 'string' || !path || !target || isAbsolute(path) || isAbsolute(target))
|
|
335
|
+
continue;
|
|
336
|
+
const at = resolvePath(app, path);
|
|
337
|
+
if (!at.startsWith(app + sep) || !resolvePath(dirname(at), target).startsWith(app + sep))
|
|
338
|
+
continue;
|
|
339
|
+
try {
|
|
340
|
+
lstatSync(at);
|
|
341
|
+
continue;
|
|
342
|
+
}
|
|
343
|
+
catch { /* missing: put it back */ }
|
|
344
|
+
try {
|
|
345
|
+
symlinkSync(target, at);
|
|
346
|
+
restored.push(path);
|
|
347
|
+
}
|
|
348
|
+
catch {
|
|
349
|
+
missing.push(path);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
return { restored, missing };
|
|
343
353
|
}
|
|
344
354
|
/**
|
|
345
355
|
* Every `shard` command (0.15.2+): when the npm package carries a newer app than the installed one (two Info.plist
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Your orchestrator's command table (0.27.0+, SDK 0.29.0): `shard orchestrator say|list|retry|dismiss`, and `restore`
|
|
3
|
+
* (0.28.0+, SDK 0.30.0). Pure data like
|
|
4
|
+
* args.ts, which merges it into COMMANDS; orchestrator-commands.ts has the handlers.
|
|
5
|
+
*/
|
|
6
|
+
import type { CommandSpec } from './args.js';
|
|
7
|
+
export declare const ORCHESTRATOR_COMMANDS: CommandSpec[];
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
const ORG = {
|
|
2
|
+
org: { type: 'string', value: '<id|slug>', description: 'Organization whose orchestrator it is (default: the saved one, shard orgs use, else your only organization). Slugs need your signed-in session.' },
|
|
3
|
+
};
|
|
4
|
+
const WHO = 'Your orchestrator is yours: one agent on its own Shardflux computer, the same on every device. Uses your signed-in session, or SHARDFLUX_API_KEY when it is set (a project key you created: a new orchestrator is filed under its project).';
|
|
5
|
+
export const ORCHESTRATOR_COMMANDS = [
|
|
6
|
+
{
|
|
7
|
+
path: ['orchestrator', 'say'],
|
|
8
|
+
summary: 'Say something to your orchestrator; one of its threads hears each input at once (0.27.0+)',
|
|
9
|
+
positionals: [{ name: 'words', variadic: true, words: true, required: true }],
|
|
10
|
+
options: {
|
|
11
|
+
task: { type: 'string', value: '<task-id>', description: 'Point at a task (task_…).' },
|
|
12
|
+
report: { type: 'string', value: '<report-id>', description: 'Point at a report (rep_…).' },
|
|
13
|
+
'needs-you': { type: 'string', value: '<item-id>', description: 'Point at something that waits on you (perm_…, oq_…).' },
|
|
14
|
+
input: { type: 'string', value: '<input-id>', description: 'Point at an earlier input (in_…).' },
|
|
15
|
+
conversation: { type: 'string', value: '<conversation-id>', description: 'Point at a conversation (conv_…).' },
|
|
16
|
+
wait: { type: 'boolean', description: 'Follow it until it is done, merged, failed or waiting, then print what it became.' },
|
|
17
|
+
timeout: { type: 'string', value: '<duration>', description: 'With --wait: stop following after this long (default 10m). The input goes on.' },
|
|
18
|
+
...ORG,
|
|
19
|
+
},
|
|
20
|
+
description: [
|
|
21
|
+
'Your exact words go to your orchestrator, with the row you point at. It answers at once: the input id and its state (queued, with its place in line; every input waits its turn in order). One of your orchestrator’s threads hears each input at once, all in parallel: a follow-up reaches the thread of the request it is about (the input reads merged), the row you point at reaches the thread of that row’s input, and anything new gets a thread of its own, which answers it, puts tasks on the board (shard board) or asks you something (shard needs-you, oq_…).',
|
|
22
|
+
'--wait follows it and prints its curated result: the answer’s headline and its points under their group headings (with when each happened) or its facts, a media line per file it shows (an image with its size in pixels), with their sources and follow-ups, the tasks it created, the line it waits on (a question names the shard needs-you answer command), or the input it was added to. Exit 6 when it failed (shard orchestrator retry sends it again), 5 when --timeout passed first.',
|
|
23
|
+
WHO,
|
|
24
|
+
'With --json: {input}, as it was stored, or with --wait as it ended.',
|
|
25
|
+
].join('\n\n'),
|
|
26
|
+
examples: ['shard orchestrator say Lets check on our language model training progress --wait', 'shard orchestrator say --task task_h7g4d7tqyv2pz3a2npyo4rpgzq use postgres instead', 'shard orchestrator say "what is blocking pricing?" --wait --json'],
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
path: ['orchestrator', 'list'],
|
|
30
|
+
summary: 'List what you said to your orchestrator and what each became, newest first (0.27.0+)',
|
|
31
|
+
positionals: [],
|
|
32
|
+
options: { all: { type: 'boolean', description: 'Also list the inputs you dismissed.' }, ...ORG },
|
|
33
|
+
description: [
|
|
34
|
+
'One line per input: its id, its state (queued with its place in line, working, waiting with what it waits on, done, merged, failed), your words, and what it became (the answer’s headline, its tasks, the input it was added to, or why it failed). Inputs still in flight are always listed.',
|
|
35
|
+
WHO,
|
|
36
|
+
'With --json: {inputs, cursor}, every input as the API answered it.',
|
|
37
|
+
].join('\n\n'),
|
|
38
|
+
examples: ['shard orchestrator list', 'shard orchestrator list --json | jq -r \'.inputs[] | select(.state=="done") | .answer.headline\''],
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
path: ['orchestrator', 'retry'],
|
|
42
|
+
summary: 'Send a failed input to your orchestrator again (0.27.0+)',
|
|
43
|
+
positionals: [{ name: 'input-id', required: true }],
|
|
44
|
+
options: ORG,
|
|
45
|
+
description: 'It goes back in line and a new thread takes it. Prints its id and state. Only a failed input is sent again.',
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
path: ['orchestrator', 'dismiss'],
|
|
49
|
+
summary: 'Take an input out of your orchestrator’s list on every device (0.27.0+)',
|
|
50
|
+
positionals: [{ name: 'input-id', required: true }],
|
|
51
|
+
options: ORG,
|
|
52
|
+
description: 'What it created stays on the board. shard orchestrator list --all still shows it; shard orchestrator restore brings it back.',
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
path: ['orchestrator', 'restore'],
|
|
56
|
+
summary: 'Bring a dismissed input back into your orchestrator’s list on every device (0.28.0+)',
|
|
57
|
+
positionals: [{ name: 'input-id', required: true }],
|
|
58
|
+
options: ORG,
|
|
59
|
+
description: [
|
|
60
|
+
'Undoes shard orchestrator dismiss: the input is listed again with what it became. Restoring an input that is not dismissed changes nothing. Questions the dismiss closed stay closed.',
|
|
61
|
+
'With --json: {input}.',
|
|
62
|
+
].join('\n\n'),
|
|
63
|
+
},
|
|
64
|
+
];
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { OrchestratorInput, OrchestratorSelection } from '@shardflux/sdk';
|
|
2
|
+
import type { Values } from './args.js';
|
|
3
|
+
import type { Handler } from './commands.js';
|
|
4
|
+
/** --wait's default: how long `say --wait` follows an input before it stops (the input goes on). */
|
|
5
|
+
export declare const SAY_WAIT_MS: number;
|
|
6
|
+
/** The row the person points at, from --task, --report, --needs-you, --input and --conversation; undefined for none. */
|
|
7
|
+
export declare function selectionOf(values: Values): OrchestratorSelection | undefined;
|
|
8
|
+
/** An input's state with what explains it: "queued (2 in line)", "waiting (Waiting for your computer: list ~/code)". */
|
|
9
|
+
export declare function stateLabel(i: OrchestratorInput): string;
|
|
10
|
+
/** What an input became, in a few words: the answer's headline, its tasks, the input it went to, or why it failed. */
|
|
11
|
+
export declare function becameText(i: OrchestratorInput): string;
|
|
12
|
+
/** One input as `shard orchestrator list` prints it: id, state, your words, what it became. */
|
|
13
|
+
export declare function inputLine(i: OrchestratorInput): string;
|
|
14
|
+
/**
|
|
15
|
+
* When an answer's point happened, short and in this computer's local time: "14:02" today, "Oct 16 14:02" this year,
|
|
16
|
+
* else "2025-10-16 14:02". A time without an offset is the person's local time and prints as written.
|
|
17
|
+
*/
|
|
18
|
+
export declare function shortTime(at: string, now?: Date): string;
|
|
19
|
+
/** An answer's body: its points under their group headings when the agent wrote points (0.27.0+), else its facts. */
|
|
20
|
+
export declare function answerLines(answer: NonNullable<OrchestratorInput['answer']>, now?: Date): string[];
|
|
21
|
+
/** One file an answer shows (0.27.0+): ` media: loss-curve.png (image, 1200×800, 84.0 KB)`, the size in pixels once known. */
|
|
22
|
+
export declare function mediaLine(m: NonNullable<NonNullable<OrchestratorInput['answer']>['media']>[number]): string;
|
|
23
|
+
/**
|
|
24
|
+
* What `say --wait` prints once an input is done, merged, failed or waiting: its curated result.
|
|
25
|
+
*
|
|
26
|
+
* Training is 73% done, on pace to finish at 16:40
|
|
27
|
+
* Moved
|
|
28
|
+
* - Loss is down to 1.84: from 2.31 at step 5,000 (14:02)
|
|
29
|
+
* - Checkpoint saved: at step 7,000
|
|
30
|
+
* Still open
|
|
31
|
+
* - Eval has not run: it starts after the last checkpoint
|
|
32
|
+
* media: loss-curve.png (image, 1200×800, 84.0 KB)
|
|
33
|
+
* Sources: train.log (~/code/lm-train/train.log), wandb.ai (https://wandb.ai/acme/lm)
|
|
34
|
+
* Follow-ups: Check again in an hour
|
|
35
|
+
* Tasks: task_… (shard task show <id>)
|
|
36
|
+
*
|
|
37
|
+
* An answer written as facts prints them as ` - <fact>` lines instead of the points. Each file the answer shows prints
|
|
38
|
+
* one media line after them (shard drive get <path> --sha256 <sha256> reads it; --json has both).
|
|
39
|
+
*/
|
|
40
|
+
export declare function resultText(i: OrchestratorInput, now?: Date): string;
|
|
41
|
+
/** Where `say --wait` stops following: nothing more happens to the input, or it waits on you or your computer. */
|
|
42
|
+
export declare const stopsWaiting: (i: OrchestratorInput) => boolean;
|
|
43
|
+
export declare const ORCHESTRATOR_HANDLERS: Record<string, Handler>;
|