@100xprompt/brain 0.1.4 → 0.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +41 -1
- package/package.json +1 -1
- package/src/adapters/amazon-q-ide/writer.ts +3 -1
- package/src/adapters/antigravity/reader.ts +25 -7
- package/src/adapters/claude-code/reader.ts +4 -0
- package/src/adapters/claude-desktop/index.ts +8 -0
- package/src/adapters/claude-desktop/reader.ts +140 -0
- package/src/adapters/claude-desktop/records.ts +191 -0
- package/src/adapters/claude-desktop/writer.ts +112 -0
- package/src/adapters/codex/reader.ts +19 -5
- package/src/adapters/codex/records.ts +17 -0
- package/src/adapters/codex/writer.ts +20 -7
- package/src/adapters/codex-desktop/index.ts +38 -0
- package/src/adapters/index.ts +4 -0
- package/src/cli.ts +79 -71
- package/src/dashboard/cli.ts +17 -2
- package/src/dashboard/report.ts +98 -5
- package/src/dashboard/sessions.ts +127 -0
- package/src/dashboard/state.ts +64 -5
- package/src/gateway/common.ts +8 -0
- package/src/gateway/connect.ts +33 -3
- package/src/gateway/connectors/antigravity.ts +205 -5
- package/src/gateway/connectors/claude-desktop.ts +169 -0
- package/src/gateway/connectors/index.ts +2 -0
- package/src/handoff.ts +18 -2
- package/src/hooks.ts +2 -2
- package/src/lock.ts +3 -0
- package/src/memory/mcp-install.ts +9 -0
- package/src/memory/sync.ts +25 -3
- package/src/moves.ts +402 -0
- package/src/service.ts +490 -0
- package/src/session/open.ts +82 -0
- package/src/session/ops.ts +111 -0
- package/src/settings/account.ts +65 -10
- package/src/settings/catalog.ts +20 -2
- package/src/settings/cli.ts +18 -3
- package/src/settings/env.ts +2 -0
- package/src/settings/pair.ts +3 -0
- package/src/settings/values.ts +28 -0
|
@@ -7,7 +7,7 @@ import { clip, header, iso, lossCounter, noteModel, toolKindOf } from "../../ses
|
|
|
7
7
|
import { withSnapshot } from "../../sqlite.ts"
|
|
8
8
|
import type { Reader, SessionRef } from "../types.ts"
|
|
9
9
|
import { summaryBody } from "./native.ts"
|
|
10
|
-
import { IMPORT_MARKER, LABEL, TOOL, idFromName, isContext, isImageLabel, isRolloutName, outputText, patchFiles } from "./records.ts"
|
|
10
|
+
import { IMPORT_MARKER, LABEL, TOOL, idFromName, isContext, isImageLabel, isRolloutName, outputText, patchFiles, isDesktopOriginator } from "./records.ts"
|
|
11
11
|
|
|
12
12
|
// Codex CLI sessions. Each thread is a rollout under ~/.codex/sessions/YYYY/MM/DD (archived ones under
|
|
13
13
|
// archived_sessions/), one JSON record per line: {timestamp, ordinal?, type, payload}, starting with
|
|
@@ -658,6 +658,7 @@ function listOne(path: string, archived: boolean, index: Map<string, ThreadRow>)
|
|
|
658
658
|
let id = idFromName(path)
|
|
659
659
|
let cwd: string | undefined
|
|
660
660
|
let version: string | undefined
|
|
661
|
+
let originator: string | undefined
|
|
661
662
|
let title: string | undefined
|
|
662
663
|
if (format === "json") {
|
|
663
664
|
// Legacy sessions are small single files and the index never had them, so read the first request here.
|
|
@@ -674,6 +675,7 @@ function listOne(path: string, archived: boolean, index: Map<string, ThreadRow>)
|
|
|
674
675
|
id = str(meta.payload?.id) ?? id
|
|
675
676
|
cwd = str(meta.payload?.cwd)
|
|
676
677
|
version = str(meta.payload?.cli_version)
|
|
678
|
+
originator = str(meta.payload?.originator)
|
|
677
679
|
}
|
|
678
680
|
} catch {
|
|
679
681
|
// Listed from its file name; read() says what is wrong with it.
|
|
@@ -691,14 +693,16 @@ function listOne(path: string, archived: boolean, index: Map<string, ThreadRow>)
|
|
|
691
693
|
...(title ? { title } : {}),
|
|
692
694
|
...(cwd ? { cwd } : {}),
|
|
693
695
|
...(updatedAt ? { updatedAt } : {}),
|
|
694
|
-
extra: { format, archived, ...(version ? { version } : {}) },
|
|
696
|
+
extra: { format, archived, ...(version ? { version } : {}), ...(originator ? { originator } : {}) },
|
|
695
697
|
}
|
|
696
698
|
}
|
|
697
699
|
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
700
|
+
/** Every Codex rollout under ~/.codex (the CLI's and the desktop apps'), newest first. */
|
|
701
|
+
export async function listRollouts(home: string): Promise<SessionRef[]> {
|
|
702
|
+
return codexReaderAll.list(home)
|
|
703
|
+
}
|
|
701
704
|
|
|
705
|
+
const codexReaderAll: Pick<Reader, "list"> = {
|
|
702
706
|
async list(home) {
|
|
703
707
|
const roots = [
|
|
704
708
|
{ dir: join(codexDir(home), "sessions"), archived: false },
|
|
@@ -714,6 +718,16 @@ export const codexReader: Reader = {
|
|
|
714
718
|
}
|
|
715
719
|
return refs.sort((a, b) => (b.updatedAt ?? "").localeCompare(a.updatedAt ?? ""))
|
|
716
720
|
},
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
export const codexReader: Reader = {
|
|
724
|
+
tool: TOOL,
|
|
725
|
+
label: LABEL,
|
|
726
|
+
|
|
727
|
+
/** The CLI's (and editors') sessions: the desktop apps' are listed as Codex Desktop's (codex-desktop). */
|
|
728
|
+
async list(home) {
|
|
729
|
+
return (await listRollouts(home)).filter((r) => !isDesktopOriginator(r.extra?.originator))
|
|
730
|
+
},
|
|
717
731
|
|
|
718
732
|
async read(ref) {
|
|
719
733
|
const path = ref.path
|
|
@@ -1,10 +1,27 @@
|
|
|
1
1
|
// Facts about Codex rollout records shared by the reader and the writer. Checked against codex-rs at
|
|
2
2
|
// rust-v0.147.0 (the version installed here) and the rollouts it wrote on this Mac; see
|
|
3
3
|
// docs/sessions/2026-11-08-session-formats.md, entries 3 and 69.
|
|
4
|
+
import { existsSync } from "node:fs"
|
|
4
5
|
|
|
5
6
|
export const TOOL = "codex"
|
|
6
7
|
export const LABEL = "Codex"
|
|
7
8
|
|
|
9
|
+
/**
|
|
10
|
+
* Codex's desktop apps start their sessions through Codex's app-server, which stamps each with the app's name as its
|
|
11
|
+
* originator (Codex's own source: a first-party client's name starts with "Codex ", e.g. "Codex Desktop"; the ChatGPT
|
|
12
|
+
* app's is "codex_chatgpt_desktop", the work app's "codex_work_desktop"). Their sessions share ~/.codex with the CLI;
|
|
13
|
+
* brain lists them as Codex Desktop's. Case matters: Codex's own test client ("codex-app-server-client-test") is not an app.
|
|
14
|
+
*/
|
|
15
|
+
const DESKTOP_IDS = new Set(["codex_desktop", "codex_chatgpt_desktop", "codex_work_desktop"])
|
|
16
|
+
export const isDesktopOriginator = (o: unknown): boolean => typeof o === "string" && (/^Codex .*\b(Desktop|App)\b/.test(o) || DESKTOP_IDS.has(o))
|
|
17
|
+
export const DESKTOP_TOOL = "codex-desktop"
|
|
18
|
+
export const DESKTOP_LABEL = "Codex Desktop"
|
|
19
|
+
/** The Codex app is on this computer (macOS: Codex.app). */
|
|
20
|
+
export function codexAppHere(home: string): boolean {
|
|
21
|
+
if (process.platform !== "darwin") return false
|
|
22
|
+
return existsSync("/Applications/Codex.app") || existsSync(`${home}/Applications/Codex.app`)
|
|
23
|
+
}
|
|
24
|
+
|
|
8
25
|
/** Codex versions whose rollout format the writer was checked against. */
|
|
9
26
|
export const TESTED_VERSIONS = ["0.147.0"]
|
|
10
27
|
|
|
@@ -88,7 +88,7 @@ export interface RolloutPlan {
|
|
|
88
88
|
/** The rollout lines for `session` as a new Codex thread `id`. Pure: writes nothing. */
|
|
89
89
|
export function planRollout(
|
|
90
90
|
session: Session,
|
|
91
|
-
opts: { id: string; now: Date; cwd: string; cliVersion: string; modelProvider: string; git?: Omit<Project, "cwd">; newId?: () => string },
|
|
91
|
+
opts: { id: string; now: Date; cwd: string; cliVersion: string; modelProvider: string; git?: Omit<Project, "cwd">; newId?: () => string; originator?: string },
|
|
92
92
|
): RolloutPlan {
|
|
93
93
|
const newId = opts.newId ?? (() => Bun.randomUUIDv7())
|
|
94
94
|
const lines: RolloutPlan["lines"] = []
|
|
@@ -118,7 +118,7 @@ export function planRollout(
|
|
|
118
118
|
session_id: opts.id,
|
|
119
119
|
timestamp: opts.now.toISOString(),
|
|
120
120
|
cwd,
|
|
121
|
-
originator: "codex-tui",
|
|
121
|
+
originator: opts.originator ?? "codex-tui",
|
|
122
122
|
cli_version: opts.cliVersion,
|
|
123
123
|
source: "cli",
|
|
124
124
|
thread_source: "user",
|
|
@@ -414,13 +414,24 @@ export type Register = (codexHome: string, threadId: string, cwd: string, name?:
|
|
|
414
414
|
/** The title the thread gets in Codex's picker: the session's own, marked with where it came from. */
|
|
415
415
|
export const threadName = (session: Session) => `${plainTitle(session.header.title ?? firstUserText(session) ?? "Session")} (from ${toolLabel(session.header.tool)})`
|
|
416
416
|
|
|
417
|
-
|
|
417
|
+
/** For a desktop app's thread: who it is (originator), whether the app is here, and how the app opens it. */
|
|
418
|
+
export interface CodexKind {
|
|
419
|
+
tool: string
|
|
420
|
+
label: string
|
|
421
|
+
originator: string
|
|
422
|
+
here(home: string): boolean
|
|
423
|
+
notHere: string
|
|
424
|
+
open(id: string, cwd: string, title: string): { open: string; note?: string }
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
export function makeCodexWriter(register: Register = registerThread, kind?: CodexKind): Writer {
|
|
418
428
|
return {
|
|
419
|
-
tool: TOOL,
|
|
420
|
-
label: LABEL,
|
|
429
|
+
tool: kind?.tool ?? TOOL,
|
|
430
|
+
label: kind?.label ?? LABEL,
|
|
421
431
|
way: "native",
|
|
422
432
|
|
|
423
433
|
async ready(opts: WriteOptions): Promise<Readiness> {
|
|
434
|
+
if (kind && !kind.here(opts.home)) return { ok: false, why: kind.notHere }
|
|
424
435
|
const v = codexVersion()
|
|
425
436
|
if (!v) return { ok: false, why: "Codex is not installed here (`codex --version` failed)" }
|
|
426
437
|
if (!TESTED_VERSIONS.includes(v))
|
|
@@ -440,6 +451,7 @@ export function makeCodexWriter(register: Register = registerThread): Writer {
|
|
|
440
451
|
cliVersion: codexVersion() ?? TESTED_VERSIONS[0]!,
|
|
441
452
|
modelProvider: configuredProvider(opts.home),
|
|
442
453
|
git: gitInfo(cwd),
|
|
454
|
+
...(kind ? { originator: kind.originator } : {}),
|
|
443
455
|
})
|
|
444
456
|
const path = rolloutPath(opts.home, id, now)
|
|
445
457
|
const lost = [...plan.lost]
|
|
@@ -463,12 +475,13 @@ export function makeCodexWriter(register: Register = registerThread): Writer {
|
|
|
463
475
|
if (!registered.ok) lost.push(`not in Codex's thread index (${registered.why}); it resumes by id, and appears in \`codex resume\` once opened`)
|
|
464
476
|
else if (registered.named === false) lost.push("thread title (Codex refused the name; the picker shows the first message instead)")
|
|
465
477
|
}
|
|
478
|
+
const opened = kind ? kind.open(id, cwd, threadName(session)) : { open: `cd ${shellQuote(cwd)} && codex resume ${id}` }
|
|
466
479
|
return {
|
|
467
|
-
tool: TOOL,
|
|
480
|
+
tool: kind?.tool ?? TOOL,
|
|
468
481
|
way: "native",
|
|
469
482
|
sessionId: id,
|
|
470
483
|
written: [path],
|
|
471
|
-
|
|
484
|
+
...opened,
|
|
472
485
|
lost,
|
|
473
486
|
dryRun: !!opts.dryRun,
|
|
474
487
|
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// Codex Desktop (OpenAI's Codex app): its threads are Codex rollouts under ~/.codex, shared with the CLI, that the app's
|
|
2
|
+
// app-server stamped with the app's name as originator (codex/records.ts). Listed and read as the app's; continued into
|
|
3
|
+
// the app as a native rollout stamped "Codex Desktop" (when the app is here and Codex's format is one brain knows), else
|
|
4
|
+
// by the continuation brief. The app opens on its own threads.
|
|
5
|
+
import { codexReader, listRollouts } from "../codex/reader.ts"
|
|
6
|
+
import { codexAppHere, DESKTOP_LABEL, DESKTOP_TOOL, isDesktopOriginator } from "../codex/records.ts"
|
|
7
|
+
import { makeCodexWriter } from "../codex/writer.ts"
|
|
8
|
+
import { shellQuote } from "../../session/open.ts"
|
|
9
|
+
import type { Reader, Writer } from "../types.ts"
|
|
10
|
+
|
|
11
|
+
export const codexDesktopReader: Reader = {
|
|
12
|
+
tool: DESKTOP_TOOL,
|
|
13
|
+
label: DESKTOP_LABEL,
|
|
14
|
+
async list(home) {
|
|
15
|
+
return (await listRollouts(home)).filter((r) => isDesktopOriginator(r.extra?.originator)).map((r) => ({ ...r, tool: DESKTOP_TOOL }))
|
|
16
|
+
},
|
|
17
|
+
async read(ref, home) {
|
|
18
|
+
const s = await codexReader.read({ ...ref, tool: "codex" }, home)
|
|
19
|
+
s.header.tool = DESKTOP_TOOL
|
|
20
|
+
s.header.id = `${DESKTOP_TOOL}:${ref.sourceId}`
|
|
21
|
+
return s
|
|
22
|
+
},
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const codexDesktopWriter = makeCodexWriter(undefined, {
|
|
26
|
+
tool: DESKTOP_TOOL,
|
|
27
|
+
label: DESKTOP_LABEL,
|
|
28
|
+
originator: "Codex Desktop",
|
|
29
|
+
here: codexAppHere,
|
|
30
|
+
notHere: "the Codex app is not on this computer",
|
|
31
|
+
open: (id, cwd, title) => ({
|
|
32
|
+
open: process.platform === "darwin" ? "open -a Codex" : "the Codex app",
|
|
33
|
+
note: `then pick "${title.slice(0, 80)}" in its threads (or in a terminal: cd ${shellQuote(cwd)} && codex resume ${id})`,
|
|
34
|
+
}),
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
export const readers: Reader[] = [codexDesktopReader]
|
|
38
|
+
export const writers: Writer[] = [codexDesktopWriter]
|
package/src/adapters/index.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { Reader, Writer } from "./types.ts";
|
|
2
2
|
import * as claudeCode from "./claude-code/index.ts";
|
|
3
|
+
import * as claudeDesktop from "./claude-desktop/index.ts";
|
|
3
4
|
import * as codex from "./codex/index.ts";
|
|
5
|
+
import * as codexDesktop from "./codex-desktop/index.ts";
|
|
4
6
|
import * as cursorIde from "./cursor-ide/index.ts";
|
|
5
7
|
import * as cursorCli from "./cursor-cli/index.ts";
|
|
6
8
|
import * as geminiCli from "./gemini-cli/index.ts";
|
|
@@ -47,7 +49,9 @@ import { briefWriter, briefWriters } from "../brief-writers.ts";
|
|
|
47
49
|
// no native or import writer is still a target through the continuation brief (brief-writers.ts).
|
|
48
50
|
const MODULES = [
|
|
49
51
|
claudeCode,
|
|
52
|
+
claudeDesktop,
|
|
50
53
|
codex,
|
|
54
|
+
codexDesktop,
|
|
51
55
|
cursorIde,
|
|
52
56
|
cursorCli,
|
|
53
57
|
geminiCli,
|
package/src/cli.ts
CHANGED
|
@@ -14,10 +14,9 @@ import { countTurns, firstUserText, toJsonl, unpaired } from "./session/util.ts"
|
|
|
14
14
|
// brain session: one working session, any AI coding tool.
|
|
15
15
|
// Design: docs/sessions/2026-11-08-session-switcher.md.
|
|
16
16
|
|
|
17
|
-
// Adapters load only for commands that use them: every tool's session-start hook runs this file, and
|
|
17
|
+
// Adapters load only for commands that use them (src/session/ops.ts): every tool's session-start hook runs this file, and
|
|
18
18
|
// one adapter failing to load (or loading slowly) must not hold up or break a session in another tool.
|
|
19
|
-
|
|
20
|
-
const adapters = async () => (loaded ??= await import("./adapters/index.ts"))
|
|
19
|
+
const adapters = async () => (await import("./session/ops.ts")).loadAdapters()
|
|
21
20
|
|
|
22
21
|
const USAGE = `brain session <command>
|
|
23
22
|
|
|
@@ -28,6 +27,7 @@ const USAGE = `brain session <command>
|
|
|
28
27
|
handoff --in <tool> [--from <tool>] [--dry-run] [--no-open] the newest session in this folder, to <tool>
|
|
29
28
|
continue/handoff also take [--lean | --full] [--budget <tokens>] [--no-memory] [--no-tidy] lean: big tool output becomes digests (automatic on long sessions); budget: mask in levels until the session fits
|
|
30
29
|
targets where a session can go, and how
|
|
30
|
+
open <id> [--print] open a session again in its own tool, in its own folder
|
|
31
31
|
hooks install|uninstall|status [--tool <id>] [--dry-run] session-start hooks that deliver briefs
|
|
32
32
|
hook <event> --tool <id> (run by a tool's hook; reads the hook's JSON on stdin)
|
|
33
33
|
|
|
@@ -45,6 +45,8 @@ brain status this computer at a glance: si
|
|
|
45
45
|
brain settings [list|get|set|unset|history|ids] brain's settings on this computer: who set each, and whether it is applied (\`brain settings help\`)
|
|
46
46
|
brain sync exchange settings with your account, apply what changed or failed, and send this computer's report now
|
|
47
47
|
brain pair [--no-browser] | status | forget approve this computer in your 100xprompt account, so its settings show in the dashboard
|
|
48
|
+
brain service install | uninstall | status run brain in the background, so changes made in your dashboard apply here by themselves
|
|
49
|
+
brain moves [approve|refuse <id>] session moves asked from your dashboard that wait for you on this computer
|
|
48
50
|
brain --version the version, and where this brain runs from
|
|
49
51
|
brain expand <blob id> the full text of a tool output that was replaced by a digest`
|
|
50
52
|
|
|
@@ -69,39 +71,25 @@ async function reportLater() {
|
|
|
69
71
|
}
|
|
70
72
|
|
|
71
73
|
async function allRefs(tool?: string): Promise<{ refs: SessionRef[]; errors: string[] }> {
|
|
72
|
-
const {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
} catch (e) {
|
|
80
|
-
errors.push(`${r.tool}: ${(e as Error).message}`)
|
|
81
|
-
return []
|
|
82
|
-
}
|
|
83
|
-
}))
|
|
84
|
-
return { refs: lists.flat().sort((a, b) => (b.updatedAt ?? "").localeCompare(a.updatedAt ?? "")), errors }
|
|
74
|
+
const { readerFor } = await adapters()
|
|
75
|
+
if (tool && !readerFor(tool)) {
|
|
76
|
+
const { READERS } = await adapters()
|
|
77
|
+
fail(`no reader for "${tool}"; readers: ${READERS.map((r) => r.tool).join(", ")}`)
|
|
78
|
+
}
|
|
79
|
+
const { listSessions } = await import("./session/ops.ts")
|
|
80
|
+
return listSessions(home, tool)
|
|
85
81
|
}
|
|
86
82
|
|
|
87
83
|
async function resolveRef(given: string): Promise<SessionRef> {
|
|
88
|
-
|
|
89
|
-
const
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
const tool = colon > 0 && readerFor(id.slice(0, colon)) ? id.slice(0, colon) : undefined
|
|
93
|
-
const want = tool ? id.slice(colon + 1) : id
|
|
94
|
-
const { refs } = await allRefs(tool)
|
|
95
|
-
const exact = refs.filter((r) => r.sourceId === want)
|
|
96
|
-
const matches = exact.length ? exact : refs.filter((r) => r.sourceId.startsWith(want))
|
|
97
|
-
if (!matches.length) fail(`no session "${id}"; see \`brain session list\``)
|
|
98
|
-
if (matches.length > 1) fail(`"${id}" matches ${matches.length} sessions (${matches.slice(0, 4).map((r) => `${r.tool}:${r.sourceId}`).join(", ")}…); give more of the id`)
|
|
99
|
-
return matches[0]!
|
|
84
|
+
const { findSession } = await import("./session/ops.ts")
|
|
85
|
+
const r = await findSession(home, given)
|
|
86
|
+
if ("error" in r) fail(r.error)
|
|
87
|
+
return (r as { ref: SessionRef }).ref
|
|
100
88
|
}
|
|
101
89
|
|
|
102
90
|
async function readRef(ref: SessionRef): Promise<Session> {
|
|
103
|
-
const {
|
|
104
|
-
return
|
|
91
|
+
const { readSession } = await import("./session/ops.ts")
|
|
92
|
+
return readSession(home, ref)
|
|
105
93
|
}
|
|
106
94
|
|
|
107
95
|
/**
|
|
@@ -154,46 +142,26 @@ function report(r: WriteResult) {
|
|
|
154
142
|
}
|
|
155
143
|
|
|
156
144
|
async function continueIn(source: Session, target: string, opts: { way?: Way; cwd?: string; dryRun: boolean; open: boolean; lean?: boolean; noMemory?: boolean; budget?: number; tidy?: false }) {
|
|
157
|
-
const {
|
|
158
|
-
const
|
|
159
|
-
|
|
160
|
-
const
|
|
161
|
-
|
|
162
|
-
const
|
|
163
|
-
for (const
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
report(r)
|
|
175
|
-
if (!r.dryRun) {
|
|
176
|
-
// The dashboard's conversion log: tools, way, title, project folder name and sizes; never the chat (src/dashboard/conversions.ts).
|
|
177
|
-
const { logConversion } = await import("./dashboard/conversions.ts")
|
|
178
|
-
const { readerFor } = await adapters()
|
|
179
|
-
const to = r.tool === "cursor" ? "cursor-ide" : r.tool
|
|
180
|
-
logConversion(home, { from: source.header.tool, fromLabel: readerFor(source.header.tool)?.label ?? toolLabel(source.header.tool), to: r.tool, toLabel: readerFor(to)?.label ?? w.label, way: r.way, title: source.header.title, cwd: source.header.project.cwd ?? cwd, turns: source.turns.length, tokens: sessionTokens(session), lean: prepared.lean })
|
|
181
|
-
}
|
|
182
|
-
if (!r.dryRun && r.way === "brief") {
|
|
183
|
-
const brief = Bun.file(r.written[0]!)
|
|
184
|
-
// The clipboard where the system has one (pbcopy on macOS, wl-copy or xclip on Linux, clip on Windows); a missing tool is fine.
|
|
185
|
-
const copy = process.platform === "darwin" ? ["pbcopy"] : process.platform === "win32" ? ["clip"] : Bun.which("wl-copy") ? ["wl-copy"] : ["xclip", "-selection", "clipboard"]
|
|
186
|
-
let copied = false
|
|
187
|
-
try {
|
|
188
|
-
if (await brief.exists()) copied = Bun.spawnSync(copy, { stdin: Buffer.from(await brief.text()) }).exitCode === 0
|
|
189
|
-
} catch {}
|
|
190
|
-
if (copied) out(" (the brief is also on the clipboard)")
|
|
191
|
-
}
|
|
192
|
-
if (!r.dryRun && opts.open && target === "cursor") Bun.spawnSync(["/bin/sh", "-c", r.open])
|
|
193
|
-
if (!r.dryRun) await reportLater()
|
|
194
|
-
return
|
|
145
|
+
const { moveSession } = await import("./session/ops.ts")
|
|
146
|
+
const { open, ...moveOpts } = opts
|
|
147
|
+
const m = await moveSession(home, source, target, moveOpts)
|
|
148
|
+
for (const n of m.notes) out(n)
|
|
149
|
+
if (!m.ok) fail(m.why)
|
|
150
|
+
const r = (m as { result: WriteResult }).result
|
|
151
|
+
for (const s of m.skipped) out(`(${s}; using the next way)`)
|
|
152
|
+
report(r)
|
|
153
|
+
if (!r.dryRun && r.way === "brief") {
|
|
154
|
+
const brief = Bun.file(r.written[0]!)
|
|
155
|
+
// The clipboard where the system has one (pbcopy on macOS, wl-copy or xclip on Linux, clip on Windows); a missing tool is fine.
|
|
156
|
+
const copy = process.platform === "darwin" ? ["pbcopy"] : process.platform === "win32" ? ["clip"] : Bun.which("wl-copy") ? ["wl-copy"] : ["xclip", "-selection", "clipboard"]
|
|
157
|
+
let copied = false
|
|
158
|
+
try {
|
|
159
|
+
if (await brief.exists()) copied = Bun.spawnSync(copy, { stdin: Buffer.from(await brief.text()) }).exitCode === 0
|
|
160
|
+
} catch {}
|
|
161
|
+
if (copied) out(" (the brief is also on the clipboard)")
|
|
195
162
|
}
|
|
196
|
-
|
|
163
|
+
if (!r.dryRun && open && target === "cursor") Bun.spawnSync(["/bin/sh", "-c", r.open])
|
|
164
|
+
if (!r.dryRun) await reportLater()
|
|
197
165
|
}
|
|
198
166
|
|
|
199
167
|
/** --lean / --full into the option (neither: decided by the session's size). */
|
|
@@ -231,7 +199,14 @@ async function main() {
|
|
|
231
199
|
}
|
|
232
200
|
if (cmd === "status") {
|
|
233
201
|
const { statusCommand } = await import("./settings/cli.ts")
|
|
234
|
-
|
|
202
|
+
let changed = false
|
|
203
|
+
const code = await statusCommand(home, undefined, undefined, { changed: () => (changed = true) })
|
|
204
|
+
if (changed) await reportLater()
|
|
205
|
+
process.exit(code)
|
|
206
|
+
}
|
|
207
|
+
if (cmd === "service") {
|
|
208
|
+
const { serviceCommand } = await import("./service.ts")
|
|
209
|
+
process.exit(await serviceCommand(rest, home))
|
|
235
210
|
}
|
|
236
211
|
if (cmd === "pair") {
|
|
237
212
|
const { pairCommand } = await import("./settings/pair.ts")
|
|
@@ -298,6 +273,25 @@ async function main() {
|
|
|
298
273
|
if (code === 0 && (rest[0] === "login" || (rest[0] === "logout" && was))) await reportLater()
|
|
299
274
|
process.exit(code)
|
|
300
275
|
}
|
|
276
|
+
if (cmd === "moves") {
|
|
277
|
+
const { decidePending, movesState, readPending } = await import("./moves.ts")
|
|
278
|
+
const sub = rest[0]
|
|
279
|
+
if (sub === "approve" || sub === "refuse") {
|
|
280
|
+
const id = rest[1] ?? fail(`moves ${sub} needs a move id (brain moves lists them)`)
|
|
281
|
+
const r = await decidePending(home, id, sub === "approve")
|
|
282
|
+
out(r.message)
|
|
283
|
+
if (r.ok && sub === "approve") await reportLater()
|
|
284
|
+
process.exit(r.ok ? 0 : 1)
|
|
285
|
+
}
|
|
286
|
+
if (sub && sub !== "list") fail(`unknown moves command "${sub}"; usage: brain moves [approve|refuse <id>]`)
|
|
287
|
+
const st = movesState(home)
|
|
288
|
+
out(`moves from the dashboard: ${st.setting === "ask" ? "asks you first" : st.setting === "allow" ? "run without asking" : "off (refused)"} (brain settings set moves.fromDashboard ask|allow|off)`)
|
|
289
|
+
const p = readPending(home).moves
|
|
290
|
+
if (!p.length) out("nothing waits for you here")
|
|
291
|
+
const { plainText } = await import("./moves.ts")
|
|
292
|
+
for (const m of p) out(` ${m.id} ${m.session}${m.title ? ` ("${plainText(m.title, 80)}")` : ""} to ${m.to}${m.way ? ` by ${m.way}` : ""} asked ${ago(m.askedAt)}: brain moves approve ${m.id} | brain moves refuse ${m.id}`)
|
|
293
|
+
process.exit(0)
|
|
294
|
+
}
|
|
301
295
|
if (cmd === "expand") {
|
|
302
296
|
const id = rest[0] ?? fail("expand needs a blob id (from a digest: \"id=...\")")
|
|
303
297
|
const { openBlobStore } = await import("./memory/blobs.ts")
|
|
@@ -315,7 +309,7 @@ async function main() {
|
|
|
315
309
|
options: {
|
|
316
310
|
tool: { type: "string" }, here: { type: "boolean" }, limit: { type: "string" }, json: { type: "boolean" },
|
|
317
311
|
in: { type: "string" }, way: { type: "string" }, cwd: { type: "string" }, from: { type: "string" },
|
|
318
|
-
"dry-run": { type: "boolean" }, "no-open": { type: "boolean" }, lean: { type: "boolean" }, full: { type: "boolean" }, "no-memory": { type: "boolean" }, budget: { type: "string" }, "no-tidy": { type: "boolean" }, o: { type: "string" }, help: { type: "boolean", short: "h" },
|
|
312
|
+
"dry-run": { type: "boolean" }, "no-open": { type: "boolean" }, print: { type: "boolean" }, lean: { type: "boolean" }, full: { type: "boolean" }, "no-memory": { type: "boolean" }, budget: { type: "string" }, "no-tidy": { type: "boolean" }, o: { type: "string" }, help: { type: "boolean", short: "h" },
|
|
319
313
|
},
|
|
320
314
|
})
|
|
321
315
|
if (!sub || values.help) return out(USAGE)
|
|
@@ -358,6 +352,20 @@ async function main() {
|
|
|
358
352
|
out(`Handing off ${ref!.tool}:${ref!.sourceId}${ref!.title ? ` ("${cut(ref!.title, 60)}")` : ""}, ${ago(ref!.updatedAt)}.`)
|
|
359
353
|
return continueIn(await readRef(ref!), target, { ...(values.way ? { way: values.way as Way } : {}), cwd: here, dryRun: !!values["dry-run"], open: !values["no-open"], ...leanOf(values), ...budgetOf(values), ...(values["no-tidy"] ? { tidy: false as const } : {}), ...(values["no-memory"] ? { noMemory: true } : {}) })
|
|
360
354
|
}
|
|
355
|
+
if (sub === "open") {
|
|
356
|
+
const id = positionals[0] ?? fail("open needs a session id")
|
|
357
|
+
const ref = await resolveRef(id)
|
|
358
|
+
const { openPlan } = await import("./session/open.ts")
|
|
359
|
+
const { readerFor } = await adapters()
|
|
360
|
+
const plan = openPlan(ref, readerFor(ref.tool)?.label ?? ref.tool, home)
|
|
361
|
+
if ("none" in plan) fail(plan.none)
|
|
362
|
+
const p = plan as { command: string; note?: string }
|
|
363
|
+
if (values.print) return out(p.command)
|
|
364
|
+
out(`Opening ${readerFor(ref.tool)?.label ?? ref.tool}${ref.title ? ` "${cut(ref.title, 60)}"` : ""}: ${p.command}`)
|
|
365
|
+
if (p.note) out(` ${p.note}`)
|
|
366
|
+
const child = Bun.spawn(["/bin/sh", "-c", p.command], { stdio: ["inherit", "inherit", "inherit"] })
|
|
367
|
+
process.exit(await child.exited)
|
|
368
|
+
}
|
|
361
369
|
if (sub === "targets") {
|
|
362
370
|
const { READERS, WRITERS, writersFor } = await adapters()
|
|
363
371
|
const targets = [...new Set(WRITERS.map((w) => w.tool))]
|
package/src/dashboard/cli.ts
CHANGED
|
@@ -59,7 +59,15 @@ export async function dashboardCommand(
|
|
|
59
59
|
args: string[],
|
|
60
60
|
home: string,
|
|
61
61
|
io: { out(l: string): void; err(l: string): void } = { out: (l) => process.stdout.write(`${l}\n`), err: (l) => process.stderr.write(`${l}\n`) },
|
|
62
|
-
deps: {
|
|
62
|
+
deps: {
|
|
63
|
+
env?: Env
|
|
64
|
+
fetchImpl?: typeof fetch
|
|
65
|
+
open?: (url: string) => void
|
|
66
|
+
countTimeoutMs?: number
|
|
67
|
+
readers?: Reader[]
|
|
68
|
+
/** Where sessions can go (runs each tool's `--version`); tests give a stand-in so they do not run the machine's tools. */
|
|
69
|
+
refreshTargets?: (home: string) => Promise<void>
|
|
70
|
+
} = {},
|
|
63
71
|
): Promise<number> {
|
|
64
72
|
const env = deps.env ?? process.env
|
|
65
73
|
const atAt = args.indexOf("--at")
|
|
@@ -83,7 +91,14 @@ export async function dashboardCommand(
|
|
|
83
91
|
const send = async (counts: boolean): Promise<void> => {
|
|
84
92
|
// The notice goes where the person reads it: the output, or stderr when the output is asked to stay quiet.
|
|
85
93
|
noticeOnce(home, quiet ? io.err : io.out)
|
|
86
|
-
const { buildReport, countAndKeep } = await import("./report.ts")
|
|
94
|
+
const { buildReport, countAndKeep, refreshTargets } = await import("./report.ts")
|
|
95
|
+
// `brain dashboard` (the person's command): where sessions can go, and the session list, now.
|
|
96
|
+
if (counts) {
|
|
97
|
+
await (deps.refreshTargets ?? refreshTargets)(home)
|
|
98
|
+
const { sendSessions } = await import("./sessions.ts")
|
|
99
|
+
const s = await sendSessions(home, { env, force: true, ...(deps.fetchImpl ? { fetchImpl: deps.fetchImpl } : {}) })
|
|
100
|
+
if (s.sent) say(`sent this computer's session list (${s.total} sessions)`)
|
|
101
|
+
}
|
|
87
102
|
const report = await buildReport(home, {
|
|
88
103
|
env,
|
|
89
104
|
...(counts ? { counts: await countAndKeep(home, { timeoutMs: deps.countTimeoutMs ?? 20_000, ...(deps.readers ? { readers: deps.readers } : {}) }) } : {}),
|