handoff-agent 1.0.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.
Files changed (4) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +42 -0
  3. package/index.js +217 -0
  4. package/package.json +37 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,42 @@
1
+ # Your agents forget everything. handoff-agent doesn't.
2
+
3
+ Capture a clean end-of-session handoff that is ready to paste into your next AI-agent run.
4
+
5
+ `npx handoff-agent`
6
+
7
+ ## What it does
8
+
9
+ - Asks 5 required questions about your current session.
10
+ - Blocks empty answers until valid input is provided.
11
+ - Writes a structured markdown handoff to `./NOTES/`.
12
+ - Uses filename format: `YYYY-MM-DD_[project-name]-handoff.md`.
13
+ - Appends a timestamp if that date+project file already exists.
14
+
15
+ ## Output format
16
+
17
+ The generated file is a context block designed to be pasted directly into the next agent session:
18
+
19
+ - YAML frontmatter for machine parsing (`handoff_version`, `date`, `project_slug`, etc.)
20
+ - Project and date metadata
21
+ - What was instructed
22
+ - What shipped
23
+ - What remains open
24
+ - Single next best instruction
25
+ - A copy/paste continuation line for the next run
26
+
27
+ ## Validate locally
28
+
29
+ `npm test`
30
+
31
+ ## Local usage
32
+
33
+ 1. Run `npx handoff-agent`
34
+ 2. Answer the prompts
35
+ 3. Open the generated file under `./NOTES/` and paste it into your next session
36
+
37
+ ## Publish to npm
38
+
39
+ 1. `npm login`
40
+ 2. `npm publish --access public`
41
+
42
+ If the package name is already taken in npm, change `"name"` in `package.json` before publishing.
package/index.js ADDED
@@ -0,0 +1,217 @@
1
+ #!/usr/bin/env node
2
+
3
+ const fs = require("fs");
4
+ const path = require("path");
5
+ const readline = require("readline");
6
+ const crypto = require("crypto");
7
+
8
+ const questions = [
9
+ "1. What project/agent is this for?",
10
+ "2. What did you instruct the agent to do today?",
11
+ "3. What did the agent ship or complete?",
12
+ "4. What failed, got skipped, or is still open?",
13
+ "5. What's the single best next instruction for the next session?",
14
+ ];
15
+
16
+ function toDateString(date) {
17
+ const year = date.getFullYear();
18
+ const month = String(date.getMonth() + 1).padStart(2, "0");
19
+ const day = String(date.getDate()).padStart(2, "0");
20
+ return `${year}-${month}-${day}`;
21
+ }
22
+
23
+ function toTimeString(date) {
24
+ const hours = String(date.getHours()).padStart(2, "0");
25
+ const minutes = String(date.getMinutes()).padStart(2, "0");
26
+ const seconds = String(date.getSeconds()).padStart(2, "0");
27
+ return `${hours}${minutes}${seconds}`;
28
+ }
29
+
30
+ function toProjectSlug(input) {
31
+ const normalized = input.normalize("NFKC").toLowerCase();
32
+ const slug = normalized
33
+ .replace(/[^\p{L}\p{N}]+/gu, "-")
34
+ .replace(/^-+|-+$/g, "");
35
+ if (slug) {
36
+ return slug;
37
+ }
38
+ const hash = crypto.createHash("sha1").update(normalized).digest("hex").slice(0, 8);
39
+ return `project-${hash}`;
40
+ }
41
+
42
+ function yamlQuote(value) {
43
+ return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
44
+ }
45
+
46
+ function askQuestion(rl, promptText) {
47
+ return new Promise((resolve) => {
48
+ const ask = () => {
49
+ rl.question(`${promptText}\n> `, (answer) => {
50
+ const value = answer.trim();
51
+ if (!value) {
52
+ console.log("Answer cannot be blank. Please try again.\n");
53
+ ask();
54
+ return;
55
+ }
56
+ resolve(value);
57
+ });
58
+ };
59
+ ask();
60
+ });
61
+ }
62
+
63
+ function parseAnswersFromText(raw, expectedCount) {
64
+ const lines = raw.split(/\r?\n/);
65
+
66
+ while (lines.length > 0 && lines[lines.length - 1] === "") {
67
+ lines.pop();
68
+ }
69
+
70
+ if (lines.length < expectedCount) {
71
+ throw new Error(
72
+ `Expected ${expectedCount} answers from stdin, received ${lines.length}.`
73
+ );
74
+ }
75
+
76
+ const answers = lines.slice(0, expectedCount).map((line) => line.trim());
77
+ const blankIndex = answers.findIndex((answer) => !answer);
78
+ if (blankIndex !== -1) {
79
+ throw new Error(`Answer ${blankIndex + 1} cannot be blank.`);
80
+ }
81
+
82
+ return answers;
83
+ }
84
+
85
+ function readAnswersFromPipedInput(expectedCount) {
86
+ const raw = fs.readFileSync(0, "utf8");
87
+ return parseAnswersFromText(raw, expectedCount);
88
+ }
89
+
90
+ function writeUniqueOutputFile(notesDir, baseStem, output, timestamp) {
91
+ let suffixAttempt = 0;
92
+
93
+ while (true) {
94
+ let fileName = `${baseStem}.md`;
95
+ if (suffixAttempt >= 1) {
96
+ fileName = `${baseStem}_${timestamp}.md`;
97
+ }
98
+ if (suffixAttempt >= 2) {
99
+ fileName = `${baseStem}_${timestamp}-${suffixAttempt - 1}.md`;
100
+ }
101
+
102
+ const outputPath = path.join(notesDir, fileName);
103
+ try {
104
+ fs.writeFileSync(outputPath, output, { encoding: "utf8", flag: "wx" });
105
+ return outputPath;
106
+ } catch (error) {
107
+ if (error && error.code === "EEXIST") {
108
+ suffixAttempt += 1;
109
+ continue;
110
+ }
111
+ throw error;
112
+ }
113
+ }
114
+ }
115
+
116
+ async function collectAnswers(rl) {
117
+ if (!process.stdin.isTTY) {
118
+ return readAnswersFromPipedInput(questions.length);
119
+ }
120
+
121
+ const answers = [];
122
+ for (const question of questions) {
123
+ const answer = await askQuestion(rl, question);
124
+ answers.push(answer);
125
+ console.log("");
126
+ }
127
+
128
+ return answers;
129
+ }
130
+
131
+ function buildOutput(now, answers, projectSlug) {
132
+ const [project, instructed, shipped, openItems, nextInstruction] = answers;
133
+ const today = toDateString(now);
134
+
135
+ return [
136
+ "---",
137
+ "handoff_version: 1",
138
+ `created_at: ${now.toISOString()}`,
139
+ `date: ${today}`,
140
+ `project: ${yamlQuote(project)}`,
141
+ `project_slug: ${projectSlug}`,
142
+ "---",
143
+ "",
144
+ "<!-- Paste this whole block into the next AI agent session -->",
145
+ "## Agent Session Handoff",
146
+ "",
147
+ `Date: ${today}`,
148
+ `Project/Agent: ${project}`,
149
+ "",
150
+ "### What I asked the agent to do today",
151
+ instructed,
152
+ "",
153
+ "### What the agent shipped/completed",
154
+ shipped,
155
+ "",
156
+ "### What failed/skipped/still open",
157
+ openItems,
158
+ "",
159
+ "### Single best next instruction",
160
+ nextInstruction,
161
+ "",
162
+ "### Copy/Paste Instruction",
163
+ "Continue this project from this handoff. Preserve shipped work, resolve open items, and execute the next instruction first.",
164
+ "",
165
+ ].join("\n");
166
+ }
167
+
168
+ function createHandoff(cwd, now, answers) {
169
+ const datePart = toDateString(now);
170
+ const projectSlug = toProjectSlug(answers[0]);
171
+ const notesDir = path.join(cwd, "NOTES");
172
+
173
+ fs.mkdirSync(notesDir, { recursive: true });
174
+
175
+ const output = buildOutput(now, answers, projectSlug);
176
+ const baseStem = `${datePart}_${projectSlug}-handoff`;
177
+ const outputPath = writeUniqueOutputFile(notesDir, baseStem, output, toTimeString(now));
178
+ return outputPath;
179
+ }
180
+
181
+ async function main() {
182
+ const rl = readline.createInterface({
183
+ input: process.stdin,
184
+ output: process.stdout,
185
+ terminal: process.stdin.isTTY && process.stdout.isTTY,
186
+ });
187
+
188
+ try {
189
+ console.log("\nCreate a handoff context block for your next agent session.\n");
190
+
191
+ const answers = await collectAnswers(rl);
192
+
193
+ const now = new Date();
194
+ const outputPath = createHandoff(process.cwd(), now, answers);
195
+
196
+ console.log("Handoff saved:");
197
+ console.log(path.relative(process.cwd(), outputPath));
198
+ } catch (error) {
199
+ console.error("Failed to create handoff:", error.message);
200
+ process.exitCode = 1;
201
+ } finally {
202
+ rl.close();
203
+ }
204
+ }
205
+
206
+ if (require.main === module) {
207
+ main();
208
+ }
209
+
210
+ module.exports = {
211
+ buildOutput,
212
+ createHandoff,
213
+ parseAnswersFromText,
214
+ readAnswersFromPipedInput,
215
+ toProjectSlug,
216
+ writeUniqueOutputFile,
217
+ };
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "handoff-agent",
3
+ "version": "1.0.0",
4
+ "description": "CLI that captures AI-agent session handoffs as ready-to-paste context markdown.",
5
+ "author": "Vishal <vishal@chaoscraftlabs.com>",
6
+ "homepage": "https://github.com/vishalgojha/handoff-agent#readme",
7
+ "bugs": {
8
+ "url": "https://github.com/vishalgojha/handoff-agent/issues"
9
+ },
10
+ "repository": {
11
+ "type": "git",
12
+ "url": "git+https://github.com/vishalgojha/handoff-agent.git"
13
+ },
14
+ "main": "index.js",
15
+ "bin": {
16
+ "handoff-agent": "index.js"
17
+ },
18
+ "scripts": {
19
+ "test": "node tests/smoke-test.js"
20
+ },
21
+ "files": [
22
+ "index.js",
23
+ "README.md",
24
+ "LICENSE"
25
+ ],
26
+ "keywords": [
27
+ "ai",
28
+ "agent",
29
+ "cli",
30
+ "handoff",
31
+ "context"
32
+ ],
33
+ "license": "MIT",
34
+ "engines": {
35
+ "node": ">=16"
36
+ }
37
+ }