@leantli/agent-handoff 0.5.1 → 0.5.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -74,8 +74,21 @@ agent-handoff learn --scope branch --kind context --note "This branch is testing
74
74
  Local cross-session memory works immediately after `agent-handoff enable`. No
75
75
  git repository is needed for the vault.
76
76
 
77
- Cross-device sync is optional. To share memory across devices, create a private
78
- git repository for the vault, then run:
77
+ Cross-device sync is optional. The vault repository should be private because it
78
+ can contain project background, preferences, decisions, and handoff notes.
79
+
80
+ If GitHub CLI (`gh`) is installed and authenticated, let `agent-handoff` create
81
+ the private repository:
82
+
83
+ ```bash
84
+ agent-handoff sync create you/agent-handoff-vault
85
+ agent-handoff sync
86
+ ```
87
+
88
+ `sync create` always uses `gh repo create --private`. If the repository already
89
+ exists and is public, `agent-handoff` refuses to use it.
90
+
91
+ If you create the repository manually, make it private, then run:
79
92
 
80
93
  ```bash
81
94
  agent-handoff sync init git@github.com:you/agent-handoff-vault.git
@@ -86,6 +99,10 @@ Run `agent-handoff sync init <same-git-url>` once on each device that should
86
99
  share the vault. After that, run `agent-handoff sync` before starting on another
87
100
  device and after writing useful checkpoints.
88
101
 
102
+ By default the vault lives at `~/.agent-handoff/vault`. Use `--home` or
103
+ `--vault` when you need a different location. `agent-handoff sync` commits local
104
+ vault changes, pulls/rebases remote vault changes, then pushes the result.
105
+
89
106
  ## How Projects Are Identified
90
107
 
91
108
  By default, `agent-handoff` identifies the current project from the git `origin`
@@ -150,6 +167,7 @@ agent-handoff enable # create local memory and install the user skill
150
167
  agent-handoff start # print context for the current project and branch
151
168
  agent-handoff checkpoint # write a session checkpoint
152
169
  agent-handoff learn # store durable global/project/branch memory
170
+ agent-handoff sync create # create a private GitHub vault repo with gh
153
171
  agent-handoff sync init # enable optional cross-device sync
154
172
  agent-handoff sync # pull/rebase and push the vault
155
173
  agent-handoff status # quick readiness and sync-state check
package/dist/cli.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { readFileSync } from "node:fs";
2
2
  import { Command, CommanderError, Option } from "commander";
3
- import { HandoffError, buildStartPacket, enableHandoff, enableSync, getStatus, learn, syncVault, writeCheckpoint, } from "./core.js";
3
+ import { HandoffError, buildStartPacket, createGitHubSyncRepo, enableHandoff, enableSync, getStatus, learn, syncVault, writeCheckpoint, } from "./core.js";
4
4
  export function main(argv = process.argv.slice(2), opts = {}) {
5
5
  const stdout = opts.stdout ?? process.stdout;
6
6
  const stderr = opts.stderr ?? process.stderr;
@@ -25,7 +25,7 @@ function buildProgram(stdout, stderr, stdin, cwd) {
25
25
  program
26
26
  .name("agent-handoff")
27
27
  .description("Shared vault handoff memory for coding agents.")
28
- .version("agent-handoff 0.5.1")
28
+ .version("agent-handoff 0.5.3")
29
29
  .exitOverride()
30
30
  .configureOutput({
31
31
  writeOut: (str) => stdout.write(str),
@@ -92,6 +92,23 @@ function buildProgram(stdout, stderr, stdin, cwd) {
92
92
  stdout.write(`Learned ${result.kind}: ${result.path}\n`);
93
93
  });
94
94
  const sync = program.command("sync").description("Sync the handoff vault.");
95
+ sync
96
+ .command("create <repository>")
97
+ .description("Create a private GitHub repository with gh and enable cross-device sync.")
98
+ .option("--vault <path>", "Vault directory. Defaults to HOME/vault.")
99
+ .addOption(new Option("--protocol <protocol>", "Git remote protocol to store in config.").choices(["https", "ssh"]).default("https"))
100
+ .action((repository, options) => {
101
+ const result = createGitHubSyncRepo({
102
+ home: globalHome(program),
103
+ vault: options.vault,
104
+ repository,
105
+ protocol: options.protocol,
106
+ });
107
+ stdout.write(`${result.created ? "Created" : "Using existing"} private GitHub repository: ${result.repository}\n`);
108
+ stdout.write(`Cross-device sync enabled: ${result.syncUrl}\n`);
109
+ stdout.write(`Vault: ${result.setup.vault}\n`);
110
+ stdout.write("Run agent-handoff sync to push local memory.\n");
111
+ });
95
112
  sync
96
113
  .command("init <git-url>")
97
114
  .description("Enable cross-device sync with a private git repository.")
package/dist/core.d.ts CHANGED
@@ -16,6 +16,13 @@ export interface EnableResult {
16
16
  setup: SetupResult;
17
17
  skill: InstallSkillResult;
18
18
  }
19
+ export interface CreateGitHubSyncRepoResult {
20
+ repository: string;
21
+ created: boolean;
22
+ isPrivate: boolean;
23
+ syncUrl: string;
24
+ setup: SetupResult;
25
+ }
19
26
  export interface CheckpointResult {
20
27
  path: string;
21
28
  projectId: string;
@@ -39,6 +46,7 @@ export interface Status {
39
46
  syncConfigured: boolean;
40
47
  syncUrl?: string;
41
48
  }
49
+ type GitHubRemoteProtocol = "https" | "ssh";
42
50
  export declare function setupHome(opts?: {
43
51
  home?: string;
44
52
  vault?: string;
@@ -58,6 +66,12 @@ export declare function enableSync(opts: {
58
66
  vault?: string;
59
67
  syncUrl: string;
60
68
  }): SetupResult;
69
+ export declare function createGitHubSyncRepo(opts: {
70
+ home?: string;
71
+ vault?: string;
72
+ repository: string;
73
+ protocol?: GitHubRemoteProtocol;
74
+ }): CreateGitHubSyncRepoResult;
61
75
  export declare function deriveProjectId(root?: string, projectId?: string): string;
62
76
  export declare function currentBranch(root?: string): string;
63
77
  export declare function buildStartPacket(opts?: {
package/dist/core.js CHANGED
@@ -130,6 +130,39 @@ export function enableHandoff(opts = {}) {
130
130
  export function enableSync(opts) {
131
131
  return setupHome({ home: opts.home, vault: opts.vault, syncUrl: opts.syncUrl });
132
132
  }
133
+ export function createGitHubSyncRepo(opts) {
134
+ const repository = normalizeGitHubRepository(opts.repository);
135
+ const protocol = opts.protocol ?? "https";
136
+ let created = false;
137
+ let info = inspectGitHubRepo(repository);
138
+ if (!info) {
139
+ ghChecked([
140
+ "repo",
141
+ "create",
142
+ repository,
143
+ "--private",
144
+ "--description",
145
+ "Private vault for agent-handoff shared coding-agent context.",
146
+ ]);
147
+ created = true;
148
+ info = inspectGitHubRepo(repository);
149
+ if (!info) {
150
+ throw new HandoffError(`created GitHub repository ${repository} but could not inspect it with gh`);
151
+ }
152
+ }
153
+ if (!info.isPrivate) {
154
+ throw new HandoffError(`GitHub repository ${repository} is public; use a private repository for agent-handoff sync`);
155
+ }
156
+ const syncUrl = githubRepoSyncUrl(info, protocol);
157
+ const setup = enableSync({ home: opts.home, vault: opts.vault, syncUrl });
158
+ return {
159
+ repository: info.nameWithOwner ?? repository,
160
+ created,
161
+ isPrivate: info.isPrivate,
162
+ syncUrl,
163
+ setup,
164
+ };
165
+ }
133
166
  export function deriveProjectId(root = ".", projectId) {
134
167
  const rootPath = resolve(root);
135
168
  if (projectId)
@@ -264,10 +297,11 @@ export function syncVault(opts = {}) {
264
297
  }
265
298
  const branch = gitOutput(setup.vault, ["branch", "--show-current"]) ?? "main";
266
299
  const pull = gitRun(setup.vault, ["pull", "--rebase", "--autostash", "origin", branch]);
267
- if (pull.status !== 0 && !isEmptyRemotePull(pull.output)) {
300
+ const ignoredEmptyRemotePull = pull.status !== 0 && isEmptyRemotePull(pull.output);
301
+ if (pull.status !== 0 && !ignoredEmptyRemotePull) {
268
302
  throw new HandoffError(pull.output.trim() || "git pull failed");
269
303
  }
270
- if (pull.output.trim())
304
+ if (!ignoredEmptyRemotePull && pull.output.trim())
271
305
  outputs.push(pull.output.trim());
272
306
  const push = gitRun(setup.vault, ["push", "-u", "origin", branch]);
273
307
  if (push.status !== 0) {
@@ -448,6 +482,55 @@ function syncConfigProblem(vault, syncUrl) {
448
482
  }
449
483
  return null;
450
484
  }
485
+ function normalizeGitHubRepository(value) {
486
+ const repository = value.trim().replace(/\/+$/g, "").replace(/\.git$/i, "");
487
+ if (repository.includes("://") || repository.startsWith("git@")) {
488
+ throw new HandoffError("GitHub repository must be in owner/name form, for example leantli/agent-handoff-vault");
489
+ }
490
+ if (!/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(repository)) {
491
+ throw new HandoffError("GitHub repository must be in owner/name form, for example leantli/agent-handoff-vault");
492
+ }
493
+ return repository;
494
+ }
495
+ function inspectGitHubRepo(repository) {
496
+ const result = ghRun(["repo", "view", repository, "--json", "nameWithOwner,isPrivate,url,sshUrl"]);
497
+ if (result.status !== 0) {
498
+ if (/could not resolve|not found|repository not found/i.test(result.output))
499
+ return null;
500
+ throw new HandoffError(result.output.trim() || `cannot inspect GitHub repository ${repository} with gh`);
501
+ }
502
+ let raw;
503
+ try {
504
+ raw = JSON.parse(result.output);
505
+ }
506
+ catch (error) {
507
+ const detail = error instanceof Error ? error.message : String(error);
508
+ throw new HandoffError(`gh repo view returned invalid JSON: ${detail}`);
509
+ }
510
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
511
+ throw new HandoffError("gh repo view returned invalid repository data");
512
+ }
513
+ const data = raw;
514
+ if (typeof data.isPrivate !== "boolean") {
515
+ throw new HandoffError("gh repo view returned invalid repository privacy data");
516
+ }
517
+ return {
518
+ nameWithOwner: typeof data.nameWithOwner === "string" ? data.nameWithOwner : undefined,
519
+ isPrivate: data.isPrivate,
520
+ url: typeof data.url === "string" ? data.url : undefined,
521
+ sshUrl: typeof data.sshUrl === "string" ? data.sshUrl : undefined,
522
+ };
523
+ }
524
+ function githubRepoSyncUrl(info, protocol) {
525
+ const raw = protocol === "ssh" ? info.sshUrl : info.url;
526
+ if (!raw) {
527
+ throw new HandoffError(`GitHub repository is missing a ${protocol} clone URL`);
528
+ }
529
+ if (raw.startsWith("https://github.com/") && !raw.endsWith(".git")) {
530
+ return `${raw}.git`;
531
+ }
532
+ return raw;
533
+ }
451
534
  function loadSetup(home) {
452
535
  const config = readConfig(home);
453
536
  if (!config)
@@ -706,6 +789,26 @@ function gitChecked(root, args) {
706
789
  }
707
790
  return result.output.trim();
708
791
  }
792
+ function ghChecked(args) {
793
+ const result = ghRun(args);
794
+ if (result.status !== 0) {
795
+ throw new HandoffError(result.output.trim() || `gh ${args.join(" ")} failed`);
796
+ }
797
+ return result.output.trim();
798
+ }
799
+ function ghRun(args) {
800
+ const result = spawnSync("gh", args, {
801
+ encoding: "utf8",
802
+ env: { ...process.env, GIT_TERMINAL_PROMPT: "0" },
803
+ stdio: ["ignore", "pipe", "pipe"],
804
+ timeout: GIT_TIMEOUT_MS,
805
+ });
806
+ const error = result.error ? `\n${result.error.message}` : "";
807
+ return {
808
+ status: result.status ?? 1,
809
+ output: `${result.stdout ?? ""}${result.stderr ?? ""}${error}`,
810
+ };
811
+ }
709
812
  function gitRun(root, args) {
710
813
  const result = spawnSync("git", args, {
711
814
  cwd: root,
package/dist/index.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- export { HandoffError, buildStartPacket, enableHandoff, enableSync, getStatus, learn, normalizeProjectId, syncVault, writeCheckpoint, } from "./core.js";
2
- export type { CheckpointResult, EnableResult, LearnResult, Status, } from "./core.js";
1
+ export { HandoffError, buildStartPacket, createGitHubSyncRepo, enableHandoff, enableSync, getStatus, learn, normalizeProjectId, syncVault, writeCheckpoint, } from "./core.js";
2
+ export type { CheckpointResult, CreateGitHubSyncRepoResult, EnableResult, LearnResult, Status, } from "./core.js";
package/dist/index.js CHANGED
@@ -1 +1 @@
1
- export { HandoffError, buildStartPacket, enableHandoff, enableSync, getStatus, learn, normalizeProjectId, syncVault, writeCheckpoint, } from "./core.js";
1
+ export { HandoffError, buildStartPacket, createGitHubSyncRepo, enableHandoff, enableSync, getStatus, learn, normalizeProjectId, syncVault, writeCheckpoint, } from "./core.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@leantli/agent-handoff",
3
- "version": "0.5.1",
3
+ "version": "0.5.3",
4
4
  "description": "Shared vault handoff memory for coding agents.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -22,6 +22,31 @@ When beginning work in a repository:
22
22
  Do not edit `AGENTS.md`, `CLAUDE.md`, or other instruction files to install
23
23
  agent-handoff.
24
24
 
25
+ ## Cross-Device Sync Setup
26
+
27
+ Use a private vault repository for sync. The vault can contain project context,
28
+ preferences, decisions, and handoff notes.
29
+
30
+ If the user asks you to create the GitHub sync repository, run:
31
+
32
+ ```bash
33
+ agent-handoff sync create <owner>/<repo>
34
+ agent-handoff sync
35
+ ```
36
+
37
+ `sync create` uses GitHub CLI (`gh`) and creates the repository as private. If an
38
+ existing repository is public, do not use it for sync.
39
+
40
+ If the user already has a private git repository for the vault, run:
41
+
42
+ ```bash
43
+ agent-handoff sync init <private-git-url>
44
+ agent-handoff sync
45
+ ```
46
+
47
+ On another device, install and enable the CLI, then run `sync init` with the same
48
+ private git URL, `agent-handoff sync`, and `agent-handoff start`.
49
+
25
50
  ## During Work
26
51
 
27
52
  Use `learn` only for stable facts that should survive future sessions, clones,