@leantli/agent-handoff 0.5.2 → 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 +20 -2
- package/dist/cli.js +19 -2
- package/dist/core.d.ts +14 -0
- package/dist/core.js +102 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/resources/agent-handoff.SKILL.md +25 -0
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.
|
|
78
|
-
|
|
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.
|
|
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)
|
|
@@ -449,6 +482,55 @@ function syncConfigProblem(vault, syncUrl) {
|
|
|
449
482
|
}
|
|
450
483
|
return null;
|
|
451
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
|
+
}
|
|
452
534
|
function loadSetup(home) {
|
|
453
535
|
const config = readConfig(home);
|
|
454
536
|
if (!config)
|
|
@@ -707,6 +789,26 @@ function gitChecked(root, args) {
|
|
|
707
789
|
}
|
|
708
790
|
return result.output.trim();
|
|
709
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
|
+
}
|
|
710
812
|
function gitRun(root, args) {
|
|
711
813
|
const result = spawnSync("git", args, {
|
|
712
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
|
@@ -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,
|