@shardflux/cli 0.24.1 → 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 +159 -1
- package/README.md +182 -35
- package/dist/account-pool.d.ts +11 -1
- package/dist/account-pool.js +48 -0
- package/dist/acp-journal.js +23 -1
- 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 +97 -14
- package/dist/board-args.js +26 -4
- package/dist/board-commands.d.ts +9 -5
- package/dist/board-commands.js +43 -16
- 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 +76 -8
- package/dist/conversation-terminal.js +2 -2
- package/dist/conversations.d.ts +2 -1
- 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 +14 -1
- package/dist/device-folders.js +20 -8
- package/dist/device-helper.d.ts +8 -0
- package/dist/device-helper.js +83 -13
- 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/errors.js +2 -1
- package/dist/format.js +6 -2
- package/dist/handlers.js +5 -2
- package/dist/land.d.ts +121 -0
- package/dist/land.js +284 -0
- package/dist/mac-app.d.ts +11 -0
- package/dist/mac-app.js +11 -0
- package/dist/main.js +9 -1
- package/dist/menubar.d.ts +18 -13
- package/dist/menubar.js +60 -50
- package/dist/onboarding.d.ts +4 -4
- 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 -10
- package/dist/router-commands.js +5 -59
- package/dist/t3-launch.js +2 -2
- package/dist/task-land-hub.py +165 -0
- 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.d.ts +7 -0
- package/dist/transcribe-commands.js +84 -0
- 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.d.ts +8 -0
- package/dist/worktree-twin-engine.js +29 -23
- package/dist/worktree-twin.d.ts +6 -0
- package/dist/worktree-twin.js +8 -1
- package/package.json +6 -5
package/dist/router-commands.js
CHANGED
|
@@ -1,67 +1,9 @@
|
|
|
1
|
-
|
|
2
|
-
* `shard route` and `shard ask` (0.25.0+): the board's input bar from a terminal, through @shardflux/sdk's
|
|
3
|
-
* `router.route()` and `questions.stream()` / `questions.ask()`. The signed-in session reads every project of an
|
|
4
|
-
* organization; SHARDFLUX_API_KEY in the environment (or a saved key without a session) reads that key's project.
|
|
5
|
-
*/
|
|
6
|
-
import { ShardfluxApiError } from '@shardflux/sdk';
|
|
7
|
-
import { UUID } from "./args.js";
|
|
8
|
-
import { findOrg, resolveOrg, resolveProject } from "./account-scope.js";
|
|
1
|
+
import { inScope } from "./person-scope.js";
|
|
9
2
|
import { out, str } from "./commands.js";
|
|
10
3
|
import { EXIT, UsageError } from "./errors.js";
|
|
11
4
|
/** The API's bound on one input or question (characters). */
|
|
12
5
|
export const BOARD_TEXT_MAX = 4000;
|
|
13
6
|
const TASK_ID = /^[A-Za-z0-9_-]{1,64}$/;
|
|
14
|
-
/** SHARDFLUX_API_KEY in the environment wins; else the signed-in session; else the saved key (its error names both). */
|
|
15
|
-
export function boardClient(ctx) {
|
|
16
|
-
if (!ctx.io.env.SHARDFLUX_API_KEY?.trim() && ctx.sessionSource() !== null)
|
|
17
|
-
return { client: ctx.account(), session: true };
|
|
18
|
-
return { client: ctx.cloud(), session: false };
|
|
19
|
-
}
|
|
20
|
-
/** --org / --project as ids (slugs resolved with the session); with neither, the saved default organization. */
|
|
21
|
-
async function scopeOf(ctx, values, b) {
|
|
22
|
-
const org = str(values.org), project = str(values.project);
|
|
23
|
-
if (!b.session) {
|
|
24
|
-
for (const [flag, v] of [['--org', org], ['--project', project]]) {
|
|
25
|
-
if (v !== undefined && !UUID.test(v))
|
|
26
|
-
throw new UsageError(`${flag} must be an id (UUID) with a project API key`, 'or sign in (shard auth login) to name it by slug');
|
|
27
|
-
}
|
|
28
|
-
return { scope: { ...(org === undefined ? {} : { organizationId: org }), ...(project === undefined ? {} : { projectId: project }) }, defaulted: false };
|
|
29
|
-
}
|
|
30
|
-
const account = b.client;
|
|
31
|
-
const scope = {};
|
|
32
|
-
if (org !== undefined)
|
|
33
|
-
scope.organizationId = UUID.test(org) ? org : (await findOrg(account, org)).id;
|
|
34
|
-
if (project !== undefined)
|
|
35
|
-
scope.projectId = UUID.test(project) ? project : (await resolveProject(ctx, account, values)).id;
|
|
36
|
-
if (org !== undefined || project !== undefined)
|
|
37
|
-
return { scope, defaulted: false };
|
|
38
|
-
const saved = ctx.store.profile(ctx.apiUrl).organization_id;
|
|
39
|
-
return { scope: saved ? { organizationId: saved } : {}, defaulted: true };
|
|
40
|
-
}
|
|
41
|
-
/**
|
|
42
|
-
* Runs `call` in the board's scope. With the session and no --org/--project: the saved default organization, else the
|
|
43
|
-
* only one; when the person is in several and none is saved (422), or the saved one is gone (404), the error lists
|
|
44
|
-
* theirs and names the fix (resolveOrg), and an organization that resolves is used.
|
|
45
|
-
*/
|
|
46
|
-
async function inScope(ctx, values, call) {
|
|
47
|
-
const b = boardClient(ctx);
|
|
48
|
-
const { scope, defaulted } = await scopeOf(ctx, values, b);
|
|
49
|
-
try {
|
|
50
|
-
return await call(b.client, scope);
|
|
51
|
-
}
|
|
52
|
-
catch (err) {
|
|
53
|
-
if (!b.session || !defaulted || !(err instanceof ShardfluxApiError))
|
|
54
|
-
throw err;
|
|
55
|
-
const several = err.status === 422 && scope.organizationId === undefined && /organization/i.test(err.message);
|
|
56
|
-
const gone = err.status === 404 && scope.organizationId !== undefined;
|
|
57
|
-
if (!several && !gone)
|
|
58
|
-
throw err;
|
|
59
|
-
const org = await resolveOrg(ctx, b.client, {});
|
|
60
|
-
if (org.id === scope.organizationId)
|
|
61
|
-
throw err;
|
|
62
|
-
return call(b.client, { organizationId: org.id });
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
7
|
function textOf(words, what, command) {
|
|
66
8
|
const text = words.join(' ').trim();
|
|
67
9
|
if (!text)
|
|
@@ -118,6 +60,8 @@ export function routeText(r, names) {
|
|
|
118
60
|
}
|
|
119
61
|
return `${lines.join('\n')}\n`;
|
|
120
62
|
}
|
|
63
|
+
/** The one-line notice `shard route` and `shard ask` print on stderr (also with --json, whose output stays the same). */
|
|
64
|
+
export const deprecationNotice = (command) => `shard ${command} is deprecated; use shard orchestrator say <words> instead.\n`;
|
|
121
65
|
const interrupted = (ctx, err) => {
|
|
122
66
|
if (ctx.signal.aborted)
|
|
123
67
|
return EXIT.interrupted;
|
|
@@ -125,6 +69,7 @@ const interrupted = (ctx, err) => {
|
|
|
125
69
|
};
|
|
126
70
|
export const ROUTER_HANDLERS = {
|
|
127
71
|
async route(ctx, values, words) {
|
|
72
|
+
ctx.io.stderr.write(deprecationNotice('route'));
|
|
128
73
|
const text = textOf(words, 'text', 'route');
|
|
129
74
|
const selectedTaskId = taskOf(values);
|
|
130
75
|
try {
|
|
@@ -144,6 +89,7 @@ export const ROUTER_HANDLERS = {
|
|
|
144
89
|
}
|
|
145
90
|
},
|
|
146
91
|
async ask(ctx, values, words) {
|
|
92
|
+
ctx.io.stderr.write(deprecationNotice('ask'));
|
|
147
93
|
const text = textOf(words, 'question', 'ask');
|
|
148
94
|
const taskId = taskOf(values);
|
|
149
95
|
const params = { text, ...(taskId === undefined ? {} : { taskId }), signal: ctx.signal };
|
package/dist/t3-launch.js
CHANGED
|
@@ -8,7 +8,7 @@ import { agentReachability } from "./agent-reachability.js";
|
|
|
8
8
|
import { conversationWorkspace, getConversation } from "./conversations.js";
|
|
9
9
|
import { resolveOn } from "./thread-on-computer.js";
|
|
10
10
|
import { ensureCloudClaude } from "./claude-cloud-login.js";
|
|
11
|
-
import { modelChoice } from "./agent-models.js";
|
|
11
|
+
import { liveModels, modelChoice } from "./agent-models.js";
|
|
12
12
|
/**
|
|
13
13
|
* A `t3_*` refusal raised once the workspace, conversation or thread exists names them (details.workspace_key,
|
|
14
14
|
* conversation_id, t3_thread_id), so a client opens what exists instead of starting it again.
|
|
@@ -32,7 +32,7 @@ export async function t3ThreadRequest(ctx, values, app) {
|
|
|
32
32
|
return undefined;
|
|
33
33
|
}
|
|
34
34
|
// The same choices as every other new thread (agent-models.ts); "default" leaves the agent's own.
|
|
35
|
-
const choice = modelChoice(values, app);
|
|
35
|
+
const choice = modelChoice(values, app, await liveModels(ctx, values, app));
|
|
36
36
|
return { app, prompt, ...(choice.model ? { model: choice.model } : {}), ...(choice.effort ? { effort: choice.effort } : {}), ...(images.length ? { images } : {}) };
|
|
37
37
|
}
|
|
38
38
|
/**
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
"""Shardflux result landing on the project hub: runs in a VM of the project (shipped and started by the shard CLI,
|
|
2
|
+
land.ts), next to the hub on shared storage.
|
|
3
|
+
|
|
4
|
+
Input: one JSON object on stdin. Output: one JSON object on stdout (exit 0; "state" says what happened).
|
|
5
|
+
|
|
6
|
+
hub the project hub (a bare repository)
|
|
7
|
+
result the result branch (shard/<task id>): the master merged its workers' branches into it
|
|
8
|
+
sources worker branches; when the result branch is missing they are merged into it here, each only when the merge
|
|
9
|
+
is clean (a conflict is the master's to resolve: state "conflict", stage "integrate")
|
|
10
|
+
default the branch to land in (default: the hub's HEAD branch, else main or master)
|
|
11
|
+
title for the merge commit's message
|
|
12
|
+
|
|
13
|
+
The result commit T lands in the default branch D: a fast-forward when D is an ancestor of T, nothing when T is already
|
|
14
|
+
in D, else a merge commit when the merge is clean (a conflict: state "conflict", stage "default"; nothing changes).
|
|
15
|
+
Commits with no history in common (a result started from another repository than the one D holds, or two workers from
|
|
16
|
+
different repositories) never merge: state "unrelated" with its stage, nothing changes. Unlike a conflict it does not
|
|
17
|
+
clear by trying again. The
|
|
18
|
+
hub's branches move only by `git push` from a temporary repository, never by force, so a branch someone else moved
|
|
19
|
+
meanwhile is refused (stage "raced") instead of overwritten. Before answering "landed" it checks on the hub itself that
|
|
20
|
+
T is contained in D, and which sources are contained in T.
|
|
21
|
+
"""
|
|
22
|
+
import json
|
|
23
|
+
import os
|
|
24
|
+
import shutil
|
|
25
|
+
import subprocess
|
|
26
|
+
import sys
|
|
27
|
+
import tempfile
|
|
28
|
+
|
|
29
|
+
IDENTITY = {'GIT_AUTHOR_NAME': 'Shardflux', 'GIT_AUTHOR_EMAIL': 'landing@shardflux.dev',
|
|
30
|
+
'GIT_COMMITTER_NAME': 'Shardflux', 'GIT_COMMITTER_EMAIL': 'landing@shardflux.dev'}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class Fail(Exception):
|
|
34
|
+
def __init__(self, reason, message):
|
|
35
|
+
super().__init__(message)
|
|
36
|
+
self.reason = reason
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def git(cwd, *args, ok=(0,), data=None):
|
|
40
|
+
env = dict(os.environ, GIT_TERMINAL_PROMPT='0', LC_ALL='C', **IDENTITY)
|
|
41
|
+
p = subprocess.run(['git', '-c', 'core.hooksPath=/dev/null', *args], cwd=cwd, env=env, input=data,
|
|
42
|
+
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
|
43
|
+
if p.returncode not in ok:
|
|
44
|
+
raise Fail('git', 'git %s failed (%d): %s' % (args[0], p.returncode, p.stderr.decode('utf-8', 'replace').strip()[-400:]))
|
|
45
|
+
return p
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def text(cwd, *args, **kw):
|
|
49
|
+
return git(cwd, *args, **kw).stdout.decode('utf-8', 'surrogateescape').strip()
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def tip(cwd, ref):
|
|
53
|
+
p = git(cwd, 'rev-parse', '-q', '--verify', ref + '^{commit}', ok=(0, 1, 128))
|
|
54
|
+
return p.stdout.decode().strip() if p.returncode == 0 else None
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def ancestor(cwd, a, b):
|
|
58
|
+
return git(cwd, 'merge-base', '--is-ancestor', a, b, ok=(0, 1)).returncode == 0
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def related(cwd, a, b):
|
|
62
|
+
"""Whether a and b have any history in common (git finds a merge base)."""
|
|
63
|
+
return git(cwd, 'merge-base', a, b, ok=(0, 1)).returncode == 0
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def merge(repo, ours, theirs, message):
|
|
67
|
+
"""A merge commit of theirs into ours when the merge is clean, else (None, conflicting paths)."""
|
|
68
|
+
r = git(repo, 'merge-tree', '--write-tree', '--name-only', '--no-messages', '-z', ours, theirs, ok=(0, 1))
|
|
69
|
+
parts = r.stdout.decode('utf-8', 'surrogateescape').split('\0')
|
|
70
|
+
if r.returncode == 1:
|
|
71
|
+
return None, sorted(set(p for p in parts[1:] if p))
|
|
72
|
+
tree = parts[0].strip()
|
|
73
|
+
return text(repo, 'commit-tree', tree, '-p', ours, '-p', theirs, '-m', message), []
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def push(repo, hub, sha, branch, old):
|
|
77
|
+
"""Moves the hub's branch from old (None: create) to sha, never by force; False when it moved meanwhile."""
|
|
78
|
+
lease = '--force-with-lease=refs/heads/%s:%s' % (branch, old or '')
|
|
79
|
+
p = git(repo, 'push', '-q', '--no-verify', lease, hub, '%s:refs/heads/%s' % (sha, branch), ok=(0, 1))
|
|
80
|
+
return p.returncode == 0
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def default_branch(hub, wanted):
|
|
84
|
+
if wanted:
|
|
85
|
+
return wanted
|
|
86
|
+
head = git(hub, 'symbolic-ref', '-q', 'HEAD', ok=(0, 1)).stdout.decode().strip()
|
|
87
|
+
if head.startswith('refs/heads/') and tip(hub, head):
|
|
88
|
+
return head[len('refs/heads/'):]
|
|
89
|
+
for name in ('main', 'master'):
|
|
90
|
+
if tip(hub, 'refs/heads/' + name):
|
|
91
|
+
return name
|
|
92
|
+
return head[len('refs/heads/'):] if head.startswith('refs/heads/') else 'main'
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def land(a):
|
|
96
|
+
hub, result, title = a['hub'], a['result'], a.get('title') or a['result']
|
|
97
|
+
sources = [s for s in a.get('sources', []) if s and s != result]
|
|
98
|
+
into = default_branch(hub, a.get('default'))
|
|
99
|
+
work = tempfile.mkdtemp(prefix='shard-result-')
|
|
100
|
+
try:
|
|
101
|
+
git(work, 'init', '-q', '--bare')
|
|
102
|
+
wanted = [result, into] + sources
|
|
103
|
+
refs = ['+refs/heads/%s:refs/heads/%s' % (b, b) for b in wanted if tip(hub, 'refs/heads/' + b)]
|
|
104
|
+
if refs:
|
|
105
|
+
git(work, 'fetch', '-q', '--no-tags', hub, *refs)
|
|
106
|
+
have = {b: tip(work, 'refs/heads/' + b) for b in wanted}
|
|
107
|
+
before = have.get(into)
|
|
108
|
+
t, created = have.get(result), False
|
|
109
|
+
if not t:
|
|
110
|
+
# A master that did not merge its workers: merge them here, each only when it merges cleanly.
|
|
111
|
+
for s in sources:
|
|
112
|
+
st = have.get(s)
|
|
113
|
+
if not st:
|
|
114
|
+
continue
|
|
115
|
+
if not t or ancestor(work, t, st):
|
|
116
|
+
t = st
|
|
117
|
+
elif not ancestor(work, st, t):
|
|
118
|
+
if not related(work, t, st):
|
|
119
|
+
return {'state': 'unrelated', 'stage': 'integrate', 'into': into, 'source': s}
|
|
120
|
+
t, conflicts = merge(work, t, st, 'Merge %s into %s' % (s, result))
|
|
121
|
+
if not t:
|
|
122
|
+
return {'state': 'conflict', 'stage': 'integrate', 'into': into, 'source': s, 'conflicts': conflicts}
|
|
123
|
+
if not t:
|
|
124
|
+
return {'state': 'missing', 'into': into, 'message': 'the project hub has no %s and no worker branch to make it from' % result}
|
|
125
|
+
if not push(work, hub, t, result, None):
|
|
126
|
+
return {'state': 'raced', 'stage': 'integrate', 'into': into, 'message': '%s appeared on the project hub meanwhile' % result}
|
|
127
|
+
created = True
|
|
128
|
+
if before and ancestor(work, t, before):
|
|
129
|
+
method, after = 'already', before
|
|
130
|
+
elif not before or ancestor(work, before, t):
|
|
131
|
+
method, after = ('created' if not before else 'ff'), t
|
|
132
|
+
else:
|
|
133
|
+
if not related(work, before, t):
|
|
134
|
+
# Another repository's branch (a project's hub holds every folder its people linked): the result still
|
|
135
|
+
# lands in its own folder; the sources it holds say which workers' work that is.
|
|
136
|
+
contained = [s for s in sources if have.get(s) and ancestor(work, have[s], t)]
|
|
137
|
+
return {'state': 'unrelated', 'stage': 'default', 'into': into, 'result': t, 'before': before, 'contained': contained}
|
|
138
|
+
after, conflicts = merge(work, before, t, 'Land %s (%s)' % (title, result))
|
|
139
|
+
if not after:
|
|
140
|
+
return {'state': 'conflict', 'stage': 'default', 'into': into, 'result': t, 'before': before, 'conflicts': conflicts}
|
|
141
|
+
method = 'merge'
|
|
142
|
+
if after != before and not push(work, hub, after, into, before):
|
|
143
|
+
return {'state': 'raced', 'stage': 'default', 'into': into, 'result': t, 'message': '%s moved on the project hub meanwhile' % into}
|
|
144
|
+
# Checked on the hub itself: the result is in the branch it landed in.
|
|
145
|
+
now = tip(hub, 'refs/heads/' + into)
|
|
146
|
+
if not now or not ancestor(hub, t, now):
|
|
147
|
+
raise Fail('unverified', '%s does not contain %s on the project hub after landing' % (into, t))
|
|
148
|
+
contained = [s for s in sources if have.get(s) and ancestor(hub, have[s], t)]
|
|
149
|
+
base = (text(hub, 'merge-base', before, t, ok=(0, 1)) or None) if before else None
|
|
150
|
+
return {'state': 'landed', 'into': into, 'before': before, 'after': now, 'result': t, 'method': method,
|
|
151
|
+
'created_result': created, 'contained': contained, 'merge_base': base}
|
|
152
|
+
finally:
|
|
153
|
+
shutil.rmtree(work, ignore_errors=True)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def main():
|
|
157
|
+
try:
|
|
158
|
+
out = land(json.load(sys.stdin))
|
|
159
|
+
except Fail as e:
|
|
160
|
+
out = {'state': 'error', 'reason': e.reason, 'message': str(e)}
|
|
161
|
+
sys.stdout.write(json.dumps(out) + '\n')
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
if __name__ == '__main__':
|
|
165
|
+
main()
|
package/dist/this-computer.d.ts
CHANGED
|
@@ -4,8 +4,10 @@ export declare const yourComputer: (platform?: NodeJS.Platform) => string;
|
|
|
4
4
|
export declare const LINUX_DEFAULT_SHORTCUT = "<Alt><Shift>space";
|
|
5
5
|
/** A GTK accelerator ("<Control><Shift>space") as the Linux app shows it ("Ctrl+Shift+Space"); shortcutLabel in its core.mjs. */
|
|
6
6
|
export declare function acceleratorLabel(accelerator: string): string;
|
|
7
|
+
/** An Electron accelerator ("Alt+Shift+Space", the one app's desktop-app.json) as the person reads it. */
|
|
8
|
+
export declare function electronAcceleratorLabel(accelerator: string, platform: NodeJS.Platform): string;
|
|
7
9
|
/**
|
|
8
|
-
* The launcher's shortcut as the person presses it:
|
|
9
|
-
* on Linux
|
|
10
|
+
* The launcher's shortcut as the person presses it: the one app's (desktop-app.json in the config directory, 0.27.0+),
|
|
11
|
+
* else ⌥⇧Space on macOS (the menu bar app's) and the Linux app's configured one on Linux; undefined elsewhere.
|
|
10
12
|
*/
|
|
11
13
|
export declare function launcherShortcut(configDir: string, platform?: NodeJS.Platform): string | undefined;
|
package/dist/this-computer.js
CHANGED
|
@@ -16,11 +16,29 @@ export function acceleratorLabel(accelerator) {
|
|
|
16
16
|
const key = accelerator.replace(/<\w+>/g, '');
|
|
17
17
|
return [...mods, key.length === 1 ? key.toUpperCase() : key.charAt(0).toUpperCase() + key.slice(1)].join('+');
|
|
18
18
|
}
|
|
19
|
+
/** An Electron accelerator ("Alt+Shift+Space", the one app's desktop-app.json) as the person reads it. */
|
|
20
|
+
export function electronAcceleratorLabel(accelerator, platform) {
|
|
21
|
+
const parts = accelerator.split('+');
|
|
22
|
+
const key = parts.pop() ?? '';
|
|
23
|
+
if (platform === 'darwin') {
|
|
24
|
+
const symbols = { Control: '⌃', Ctrl: '⌃', Alt: '⌥', Option: '⌥', Shift: '⇧', Command: '⌘', Cmd: '⌘', CommandOrControl: '⌘', CmdOrCtrl: '⌘' };
|
|
25
|
+
const order = ['⌃', '⌥', '⇧', '⌘'];
|
|
26
|
+
return parts.map(p => symbols[p] ?? p).sort((a, b) => order.indexOf(a) - order.indexOf(b)).join('') + key;
|
|
27
|
+
}
|
|
28
|
+
const names = { Control: 'Ctrl', CommandOrControl: 'Ctrl', CmdOrCtrl: 'Ctrl', Command: 'Super', Cmd: 'Super', Super: 'Super', Meta: 'Super' };
|
|
29
|
+
return [...parts.map(p => names[p] ?? p), key].join('+');
|
|
30
|
+
}
|
|
19
31
|
/**
|
|
20
|
-
* The launcher's shortcut as the person presses it:
|
|
21
|
-
* on Linux
|
|
32
|
+
* The launcher's shortcut as the person presses it: the one app's (desktop-app.json in the config directory, 0.27.0+),
|
|
33
|
+
* else ⌥⇧Space on macOS (the menu bar app's) and the Linux app's configured one on Linux; undefined elsewhere.
|
|
22
34
|
*/
|
|
23
35
|
export function launcherShortcut(configDir, platform = process.platform) {
|
|
36
|
+
try {
|
|
37
|
+
const app = JSON.parse(readFileSync(join(configDir, 'desktop-app.json'), 'utf8'));
|
|
38
|
+
if (typeof app.launcher_shortcut === 'string' && /^([A-Za-z]+\+)+\S+$/.test(app.launcher_shortcut) && (platform === 'darwin' || platform === 'linux'))
|
|
39
|
+
return electronAcceleratorLabel(app.launcher_shortcut, platform);
|
|
40
|
+
}
|
|
41
|
+
catch { /* the app has not written it (an older app, or none) */ }
|
|
24
42
|
if (platform === 'darwin')
|
|
25
43
|
return '⌥⇧Space';
|
|
26
44
|
if (platform !== 'linux')
|
|
@@ -19,7 +19,7 @@ import { cloudClaudeLogin, loginClaudeInWorkspace, requireClaudeTerminal } from
|
|
|
19
19
|
import { promptWithImages, readInitialImages, readInitialPrompt, uploadInitialImages } from "./initial-prompt.js";
|
|
20
20
|
import { recordSeedSnapshot } from "./files-mirror.js";
|
|
21
21
|
import { ensureCodexCloud, printCloudThread, submitCloudInitialPrompt } from "./codex-cloud.js";
|
|
22
|
-
import { modelChoice } from "./agent-models.js";
|
|
22
|
+
import { liveModels, modelChoice } from "./agent-models.js";
|
|
23
23
|
import { storedTitlePrefix } from "./client-settings.js";
|
|
24
24
|
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
25
25
|
const SHA = /^[0-9a-f]{40}(?:[0-9a-f]{24})?$/;
|
|
@@ -143,7 +143,7 @@ export async function startThreadOn(ctx, values, path, explicitApp) {
|
|
|
143
143
|
const review = values.review === true;
|
|
144
144
|
if (review && app === 'codex')
|
|
145
145
|
throw new UsageError('--review starts a read-only Claude reviewer. A Codex thread on a computer works on its own branch; omit --review.');
|
|
146
|
-
const choice = modelChoice(values, app);
|
|
146
|
+
const choice = modelChoice(values, app, await liveModels(ctx, values, app));
|
|
147
147
|
const name = str(values.name);
|
|
148
148
|
if (name !== undefined && !/^[a-z0-9][a-z0-9-]{0,47}$/.test(name))
|
|
149
149
|
throw new UsageError('--name must match ^[a-z0-9][a-z0-9-]{0,47}$');
|
package/dist/timing.js
CHANGED
|
@@ -53,13 +53,15 @@ export class TimingRecorder {
|
|
|
53
53
|
return own && own.action !== 'token' ? own : this.last;
|
|
54
54
|
}
|
|
55
55
|
}
|
|
56
|
-
/** A resume that booted
|
|
56
|
+
/** A resume that booted its disk: files kept, every process restarted (never a null memory_restored). */
|
|
57
57
|
function restartedProcesses(t) {
|
|
58
58
|
return t.server?.resumePath === 'cold_boot' && t.server.memoryRestored === false;
|
|
59
59
|
}
|
|
60
60
|
/** Cold boot reasons the notice says in words (0.8.1+); any other reason is printed as the API sent it. */
|
|
61
61
|
const COLD_BOOT_WORDS = {
|
|
62
62
|
host_lost: 'the machine it ran on failed',
|
|
63
|
+
guest_unresponsive: 'the guest did not become ready after bounded restore attempts',
|
|
64
|
+
guest_filesystem: 'the filesystem was repaired offline',
|
|
63
65
|
};
|
|
64
66
|
/**
|
|
65
67
|
* The one-line stderr notice after a command whose resume booted the workspace's saved disk (0.6.0+), with or without
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { Transcript } from '@shardflux/sdk';
|
|
2
|
+
import type { Handler } from './commands.js';
|
|
3
|
+
/** What one transcription holds (the API's bound; past it the rest is not transcribed and `truncated` says so). */
|
|
4
|
+
export declare const TRANSCRIBE_MAX_BYTES: number;
|
|
5
|
+
/** The readable form: the text, then (with --words) one line per word with its start and end. */
|
|
6
|
+
export declare function transcriptText(t: Transcript, words: boolean): string;
|
|
7
|
+
export declare const TRANSCRIBE_HANDLERS: Record<string, Handler>;
|
|
@@ -0,0 +1,84 @@
|
|
|
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 { ShardfluxApiError, audioFormatOf } from '@shardflux/sdk';
|
|
9
|
+
import { out, str } from "./commands.js";
|
|
10
|
+
import { EXIT, UsageError } from "./errors.js";
|
|
11
|
+
import { inScope } from "./person-scope.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
|
+
if (err instanceof ShardfluxApiError && err.reason === 'cloud_transcription_off') {
|
|
78
|
+
ctx.io.stderr.write('Cloud transcription is not on for this organization (cloud_transcription_off).\n');
|
|
79
|
+
return EXIT.error;
|
|
80
|
+
}
|
|
81
|
+
throw err;
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
};
|
package/dist/update.js
CHANGED
|
@@ -18,12 +18,14 @@ import { AgentStateStore } from "./agent-state.js";
|
|
|
18
18
|
import { writePrivateFile } from "./credentials.js";
|
|
19
19
|
import { CodedError } from "./errors.js";
|
|
20
20
|
import { json } from "./format.js";
|
|
21
|
+
import { macAppPackage, ONE_APP_ON_MAC } from "./mac-app.js";
|
|
21
22
|
import { compareAppVersions, MenubarManager, packagedAppPath } from "./menubar.js";
|
|
22
23
|
export const INSTALL_FILE = 'install.json';
|
|
23
24
|
export const LAST_UPDATE_FILE = 'last-update.json';
|
|
24
25
|
const LOCK_FILE = 'update.lock';
|
|
25
26
|
const MCP = '@shardflux/mcp';
|
|
26
|
-
|
|
27
|
+
/** The npm package of the macOS app on this Mac: the menu bar app, or the one Shardflux app once it takes over. */
|
|
28
|
+
const MENUBAR = macAppPackage();
|
|
27
29
|
/** Finds the package directory of `pkg` above `script` and how it was installed. */
|
|
28
30
|
export function detectInstall(script, pkg, platform = process.platform) {
|
|
29
31
|
let real = script;
|
|
@@ -100,7 +102,7 @@ export function displayCommand(install, specs) {
|
|
|
100
102
|
}
|
|
101
103
|
/** Prefixes can contain spaces, quotes or shell substitutions. The actual install still uses argv. */
|
|
102
104
|
const shellQuote = (value) => `'${value.replaceAll("'", "'\\''")}'`;
|
|
103
|
-
const LABELS = { shardflux: 'shardflux', '@shardflux/cli': 'shard', '@shardflux/sdk': 'SDK', [MENUBAR]: 'menu bar', [MCP]: 'MCP server' };
|
|
105
|
+
const LABELS = { shardflux: 'shardflux', '@shardflux/cli': 'shard', '@shardflux/sdk': 'SDK', [MENUBAR]: ONE_APP_ON_MAC ? 'Shardflux app' : 'menu bar', [MCP]: 'MCP server' };
|
|
104
106
|
/** Every local piece and its status against the catalog. */
|
|
105
107
|
export async function components(ctx, install, catalog, menubar = menubarManager(ctx)) {
|
|
106
108
|
const local = [[ctx.distribution.package, ctx.distribution.version]];
|
package/dist/wait-status.d.ts
CHANGED
|
@@ -14,7 +14,7 @@ export declare function queueLine(e: Pick<Extract<ProgressEvent, {
|
|
|
14
14
|
type: 'phase';
|
|
15
15
|
}>, 'queuePosition' | 'scaleOut' | 'expectedReadyBy' | 'deadlineAt'>): string;
|
|
16
16
|
/** A call waiting to be admitted, in words. */
|
|
17
|
-
export declare function admissionLine(r: Pick<RetryRecord, 'reason' | 'queuePosition'>): string;
|
|
17
|
+
export declare function admissionLine(r: Pick<RetryRecord, 'reason' | 'queuePosition' | 'retryAfterSeconds'>): string;
|
|
18
18
|
/**
|
|
19
19
|
* The client listener that prints where a wait stands. Silent with `json`. A phase other than capacity_pending ends
|
|
20
20
|
* the queue, so a later queue prints again.
|
package/dist/wait-status.js
CHANGED
|
@@ -17,6 +17,8 @@ export function queueLine(e) {
|
|
|
17
17
|
/** A call waiting to be admitted, in words. */
|
|
18
18
|
export function admissionLine(r) {
|
|
19
19
|
const position = r.queuePosition ? ` (position ${r.queuePosition})` : '';
|
|
20
|
+
if (r.reason === 'admission_queued')
|
|
21
|
+
return `queued for admission${position}${r.retryAfterSeconds != null ? `; retrying in ${r.retryAfterSeconds} s` : ''}`;
|
|
20
22
|
if (r.reason === 'host_capacity')
|
|
21
23
|
return `waiting for room on the workspace's host${position}`;
|
|
22
24
|
if (r.reason === 'concurrent_workspaces')
|
|
@@ -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 }); },
|
|
@@ -618,7 +624,7 @@ export const TWIN_HANDLERS = {
|
|
|
618
624
|
ledger.setMode(project, action);
|
|
619
625
|
}
|
|
620
626
|
const mode = ledger.mode(project);
|
|
621
|
-
const text = mode === 'auto' ? 'Finished conversations land in your local branch automatically.' : mode === 'pr' ? 'Finished conversations open a pull request instead of landing locally.' : 'Automatic landing is off for this project.';
|
|
627
|
+
const text = !ledger.autoLand() ? 'Automatic landing is off on this computer: shard settings --auto-land on turns it back on.' : mode === 'auto' ? 'Finished conversations land in your local branch automatically.' : mode === 'pr' ? 'Finished conversations open a pull request instead of landing locally.' : 'Automatic landing is off for this project.';
|
|
622
628
|
out(ctx, `${text}\n`, { project_id: project, landing: mode });
|
|
623
629
|
return 0;
|
|
624
630
|
},
|
package/dist/worktree-twin.d.ts
CHANGED
|
@@ -265,7 +265,9 @@ export interface TwinConversationState {
|
|
|
265
265
|
last_error?: string;
|
|
266
266
|
latency_ms?: number;
|
|
267
267
|
}
|
|
268
|
+
/** `auto_land: false` (`shard settings --auto-land off`, the desktop app's Settings switch): nothing lands on this computer. */
|
|
268
269
|
interface TwinDoc {
|
|
270
|
+
auto_land?: boolean;
|
|
269
271
|
landing?: Record<string, LandingMode>;
|
|
270
272
|
conversations?: Record<string, TwinConversationState>;
|
|
271
273
|
}
|
|
@@ -273,8 +275,12 @@ export declare class TwinLedger {
|
|
|
273
275
|
readonly store: AgentStateStore;
|
|
274
276
|
constructor(dir: string);
|
|
275
277
|
read(): TwinDoc;
|
|
278
|
+
/** The project's landing, 'off' everywhere while this computer's switch is off. */
|
|
276
279
|
mode(project: string): LandingMode;
|
|
277
280
|
setMode(project: string, mode: LandingMode): void;
|
|
281
|
+
/** This computer's switch for every project (on unless turned off). */
|
|
282
|
+
autoLand(): boolean;
|
|
283
|
+
setAutoLand(on: boolean): void;
|
|
278
284
|
conversation(id: string): TwinConversationState;
|
|
279
285
|
patch(id: string, change: Partial<TwinConversationState>): void;
|
|
280
286
|
forget(id: string): void;
|
package/dist/worktree-twin.js
CHANGED
|
@@ -726,8 +726,15 @@ export class TwinLedger {
|
|
|
726
726
|
store;
|
|
727
727
|
constructor(dir) { this.store = new AgentStateStore(dir); }
|
|
728
728
|
read() { return this.store.read().worktree_twin ?? {}; }
|
|
729
|
-
|
|
729
|
+
/** The project's landing, 'off' everywhere while this computer's switch is off. */
|
|
730
|
+
mode(project) { const doc = this.read(); return doc.auto_land === false ? 'off' : doc.landing?.[project] ?? 'auto'; }
|
|
730
731
|
setMode(project, mode) { this.update(doc => { doc.landing = { ...doc.landing, [project]: mode }; }); }
|
|
732
|
+
/** This computer's switch for every project (on unless turned off). */
|
|
733
|
+
autoLand() { return this.read().auto_land !== false; }
|
|
734
|
+
setAutoLand(on) { this.update(doc => { if (on)
|
|
735
|
+
delete doc.auto_land;
|
|
736
|
+
else
|
|
737
|
+
doc.auto_land = false; }); }
|
|
731
738
|
conversation(id) { return this.read().conversations?.[id] ?? {}; }
|
|
732
739
|
patch(id, change) {
|
|
733
740
|
this.update(doc => { doc.conversations = { ...doc.conversations, [id]: { ...doc.conversations?.[id], ...change } }; });
|