pipit-cli 0.1.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 +118 -0
  3. package/dist/cli.js +729 -0
  4. package/package.json +54 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Maximilian Maksutovic
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,118 @@
1
+ # pipit
2
+
3
+ Pipe any context into Claude Code sessions.
4
+
5
+ Screenshots, meeting notes, URLs, clipboard text -- capture it in one command, and Claude Code extracts tasks, plans implementation, and gets to work.
6
+
7
+ ```
8
+ pipit go notes.md my-project
9
+ ```
10
+
11
+ That's it. pipit opens a new terminal tab, feeds your context to Claude Code with a carefully crafted prompt, and Claude handles the rest -- extracting action items, planning, and implementing.
12
+
13
+ ## Install
14
+
15
+ ```bash
16
+ npm install -g pipit-cli
17
+ ```
18
+
19
+ Requires **Node.js 20+** and [Claude Code](https://claude.ai/code) installed.
20
+
21
+ ## Usage
22
+
23
+ ### `pipit go` -- send context to Claude Code
24
+
25
+ ```bash
26
+ # File (text, markdown, transcript)
27
+ pipit go meeting-notes.md my-project
28
+
29
+ # Screenshot or image
30
+ pipit go screenshot.png my-project
31
+
32
+ # URL
33
+ pipit go https://example.com/spec my-project
34
+
35
+ # Stdin
36
+ cat notes.txt | pipit go - my-project
37
+ ```
38
+
39
+ pipit infers the input type automatically. Override with `--input-type` if needed.
40
+
41
+ ### Options
42
+
43
+ ```
44
+ pipit go <input> <project-name> [options]
45
+
46
+ Options:
47
+ -p, --project-dir <dir> Project directory (repeatable)
48
+ --input-type <type> Override input type: file, image, url, stdin
49
+ --control-level <level> auto (default), plan_first, extract_only
50
+ --mode <mode> fire_and_forget (default), interactive
51
+ --terminal <name> ghostty, iterm2, warp, terminal
52
+ --context <text> Additional instructions for Claude
53
+ --dry-run Print the prompt without launching
54
+ --json Machine-readable JSON output
55
+ ```
56
+
57
+ ### Control levels
58
+
59
+ - **auto** -- Claude gets full autonomy (`--dangerously-skip-permissions`). Context goes in, code comes out.
60
+ - **plan_first** -- Claude extracts and plans, then pauses for your approval before implementing.
61
+ - **extract_only** -- Just show the extracted tasks. You decide what to do next.
62
+
63
+ ### Session modes
64
+
65
+ - **fire_and_forget** -- Claude works autonomously. You watch and intervene if needed.
66
+ - **interactive** -- Claude interviews you first, then plans and implements collaboratively.
67
+
68
+ ## How it works
69
+
70
+ ```
71
+ Your context (file, image, URL, clipboard)
72
+ |
73
+ v
74
+ pipit CLI (builds prompt, resolves input)
75
+ |
76
+ v
77
+ Terminal tab (Ghostty, iTerm2, Warp, Terminal.app)
78
+ |
79
+ v
80
+ Claude Code (extracts tasks, plans, implements, commits)
81
+ |
82
+ v
83
+ Results (branches, commits, PRs)
84
+ ```
85
+
86
+ pipit doesn't preprocess your input or run a separate extraction pipeline. It builds a prompt and hands everything to Claude Code in an interactive terminal session. Claude is smart enough to handle raw context directly.
87
+
88
+ ## Terminal support
89
+
90
+ pipit opens Claude Code sessions in new terminal tabs via AppleScript:
91
+
92
+ - **Ghostty** (default) -- native API
93
+ - **iTerm2** -- AppleScript integration
94
+ - **Warp** -- AppleScript integration
95
+ - **Terminal.app** -- always available as fallback
96
+
97
+ Set the default with `--terminal` or the `PIPIT_TERMINAL` environment variable.
98
+
99
+ ## Mac app
100
+
101
+ pipit also has a native macOS menu bar app that provides:
102
+
103
+ - Global hotkey to capture clipboard and launch a session
104
+ - File watchers for screenshots and transcripts
105
+ - Project management with multi-directory support
106
+ - One-click CLI installation
107
+
108
+ The Mac app calls the CLI under the hood -- all the logic lives here.
109
+
110
+ ## Requirements
111
+
112
+ - **Node.js 20+**
113
+ - **Claude Code** CLI installed and authenticated
114
+ - **macOS** (terminal automation uses AppleScript)
115
+
116
+ ## License
117
+
118
+ MIT
package/dist/cli.js ADDED
@@ -0,0 +1,729 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli.ts
4
+ import { Command, Option } from "commander";
5
+ import { access as access2 } from "fs/promises";
6
+ import { resolve as resolve3 } from "path";
7
+
8
+ // src/extractor/extractor.ts
9
+ import { readFile } from "fs/promises";
10
+ import { resolve } from "path";
11
+ import { execa } from "execa";
12
+
13
+ // src/extractor/types.ts
14
+ import { z } from "zod";
15
+ var ActionItemSchema = z.object({
16
+ id: z.string(),
17
+ title: z.string(),
18
+ description: z.string(),
19
+ priority: z.enum(["high", "medium", "low"]),
20
+ estimated_complexity: z.enum(["trivial", "small", "medium", "large"]),
21
+ relevant_files: z.array(z.string()),
22
+ transcript_context: z.string()
23
+ });
24
+ var ExtractionResultSchema = z.object({
25
+ meeting_summary: z.string(),
26
+ action_items: z.array(ActionItemSchema),
27
+ decisions: z.array(z.string()),
28
+ questions_to_clarify: z.array(z.string()),
29
+ claude_md_updates: z.array(z.string())
30
+ });
31
+ function getExtractionJsonSchema() {
32
+ return JSON.stringify({
33
+ type: "object",
34
+ properties: {
35
+ meeting_summary: { type: "string" },
36
+ action_items: {
37
+ type: "array",
38
+ items: {
39
+ type: "object",
40
+ properties: {
41
+ id: { type: "string" },
42
+ title: { type: "string" },
43
+ description: { type: "string" },
44
+ priority: { type: "string", enum: ["high", "medium", "low"] },
45
+ estimated_complexity: { type: "string", enum: ["trivial", "small", "medium", "large"] },
46
+ relevant_files: { type: "array", items: { type: "string" } },
47
+ transcript_context: { type: "string" }
48
+ },
49
+ required: ["id", "title", "description", "priority", "estimated_complexity", "relevant_files", "transcript_context"]
50
+ }
51
+ },
52
+ decisions: { type: "array", items: { type: "string" } },
53
+ questions_to_clarify: { type: "array", items: { type: "string" } },
54
+ claude_md_updates: { type: "array", items: { type: "string" } }
55
+ },
56
+ required: ["meeting_summary", "action_items", "decisions", "questions_to_clarify", "claude_md_updates"]
57
+ });
58
+ }
59
+
60
+ // src/extractor/extractor.ts
61
+ var EXTRACTION_SYSTEM_PROMPT = `You are analyzing a meeting transcript to extract actionable development tasks.
62
+
63
+ For each task you identify:
64
+ - Give it a unique id (use kebab-case, e.g. "add-user-auth", "fix-api-timeout")
65
+ - Write a clear title and detailed description of what needs to be implemented
66
+ - Assess priority (high/medium/low) based on urgency signals in the conversation
67
+ - Estimate complexity (trivial/small/medium/large) based on scope
68
+ - List any files mentioned or implied as relevant
69
+ - Include the relevant transcript excerpt that prompted this task
70
+
71
+ Also extract:
72
+ - A brief meeting summary
73
+ - Key decisions made during the meeting
74
+ - Questions that need clarification before implementation
75
+ - Any updates that should be made to the project's CLAUDE.md`;
76
+ async function extractTasks(transcriptPath, projectDir) {
77
+ const absolutePath = resolve(transcriptPath);
78
+ const transcript = await readFile(absolutePath, "utf-8");
79
+ const jsonSchema = getExtractionJsonSchema();
80
+ const { stdout } = await execa("claude", [
81
+ "-p",
82
+ EXTRACTION_SYSTEM_PROMPT,
83
+ "--allowedTools",
84
+ "Read,Glob,Grep",
85
+ "--output-format",
86
+ "json",
87
+ "--json-schema",
88
+ jsonSchema
89
+ ], {
90
+ input: transcript,
91
+ cwd: resolve(projectDir),
92
+ timeout: 3e5
93
+ });
94
+ const envelope = JSON.parse(stdout);
95
+ if (envelope.is_error) {
96
+ throw new Error(`claude -p failed: ${envelope.result || "unknown error"}`);
97
+ }
98
+ const data = envelope.structured_output ?? envelope.result;
99
+ if (!data) {
100
+ throw new Error("claude -p returned no structured output");
101
+ }
102
+ const parsed = typeof data === "string" ? JSON.parse(data) : data;
103
+ const result = ExtractionResultSchema.parse(parsed);
104
+ return result;
105
+ }
106
+
107
+ // src/executor/worker.ts
108
+ import { execa as execa2 } from "execa";
109
+ import path from "path";
110
+ function generateSlug(title) {
111
+ return title.toLowerCase().replace(/[^a-z0-9\s-]/g, "").replace(/[\s-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 50).replace(/-+$/, "");
112
+ }
113
+ async function createWorktree(projectDir, slug) {
114
+ const worktreePath = path.join(projectDir, ".worktrees", `pipit-${slug}`);
115
+ const branchName = `pipit/${slug}`;
116
+ await execa2("git", ["worktree", "add", worktreePath, "-b", branchName], {
117
+ cwd: projectDir
118
+ });
119
+ return worktreePath;
120
+ }
121
+ async function cleanupWorktree(projectDir, worktreePath) {
122
+ try {
123
+ await execa2("git", ["worktree", "remove", worktreePath, "--force"], {
124
+ cwd: projectDir
125
+ });
126
+ } catch {
127
+ }
128
+ }
129
+ function escapeAppleScript(str) {
130
+ return str.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
131
+ }
132
+ function buildAppleScript(terminal, worktreePath, claudeCommand) {
133
+ switch (terminal) {
134
+ case "ghostty": {
135
+ const escapedPath = escapeAppleScript(worktreePath);
136
+ const escapedCmd = escapeAppleScript(claudeCommand);
137
+ return `
138
+ tell application "Ghostty"
139
+ activate
140
+ set cfg to new surface configuration
141
+ set initial working directory of cfg to "${escapedPath}"
142
+ set initial input of cfg to "${escapedCmd}\\n"
143
+ new tab in front window with configuration cfg
144
+ end tell
145
+ `;
146
+ }
147
+ case "iterm2": {
148
+ const escapedCmd = escapeAppleScript(`cd '${worktreePath}' && ${claudeCommand}`);
149
+ return `
150
+ tell application "iTerm2"
151
+ tell current window
152
+ create tab with default profile
153
+ tell current session of current tab
154
+ write text "${escapedCmd}"
155
+ end tell
156
+ end tell
157
+ end tell
158
+ `;
159
+ }
160
+ case "warp":
161
+ case "apple-terminal":
162
+ default: {
163
+ const escapedCmd = escapeAppleScript(`cd '${worktreePath}' && ${claudeCommand}`);
164
+ return `
165
+ tell application "Terminal"
166
+ activate
167
+ do script "${escapedCmd}"
168
+ end tell
169
+ `;
170
+ }
171
+ }
172
+ }
173
+ function resolveTerminal() {
174
+ const override = process.env.PIPIT_TERMINAL?.toLowerCase();
175
+ if (override === "iterm2") return "iterm2";
176
+ if (override === "warp") return "warp";
177
+ if (override === "terminal") return "apple-terminal";
178
+ return "ghostty";
179
+ }
180
+ async function launchClaudeTab(workingDir, claudeCommand) {
181
+ const terminal = resolveTerminal();
182
+ const script = buildAppleScript(terminal, workingDir, claudeCommand);
183
+ await execa2("osascript", ["-e", script]);
184
+ }
185
+ async function implementTask(config) {
186
+ const { task, projectDir, transcriptContext } = config;
187
+ const slug = generateSlug(task.title);
188
+ const branchName = `pipit/${slug}`;
189
+ let worktreePath = "";
190
+ try {
191
+ worktreePath = await createWorktree(projectDir, slug);
192
+ const prompt = [
193
+ `## Task: ${task.title}`,
194
+ "",
195
+ `### Description`,
196
+ task.description,
197
+ "",
198
+ `### Transcript Context`,
199
+ transcriptContext,
200
+ "",
201
+ `### Instructions`,
202
+ `Implement the task described above. Follow existing code conventions.`,
203
+ `If relevant files were identified, start there: ${task.relevant_files.join(", ") || "none specified"}.`,
204
+ `When done, commit your changes with a descriptive message.`
205
+ ].join("\n");
206
+ const escapedPrompt = prompt.replace(/'/g, "'\\''");
207
+ const claudeCommand = `claude --dangerously-skip-permissions '${escapedPrompt}'`;
208
+ await launchClaudeTab(worktreePath, claudeCommand);
209
+ return {
210
+ success: true,
211
+ branch_name: branchName,
212
+ files_changed: [],
213
+ worktree_path: worktreePath
214
+ };
215
+ } catch (err) {
216
+ if (worktreePath) {
217
+ await cleanupWorktree(projectDir, worktreePath);
218
+ }
219
+ const message = err instanceof Error ? err.message : "Unknown error launching implementation session";
220
+ return {
221
+ success: false,
222
+ branch_name: branchName,
223
+ files_changed: [],
224
+ worktree_path: worktreePath,
225
+ error: message
226
+ };
227
+ }
228
+ }
229
+
230
+ // src/input/resolver.ts
231
+ import { access } from "fs/promises";
232
+ import { resolve as resolve2, extname } from "path";
233
+ var IMAGE_EXTENSIONS = /* @__PURE__ */ new Set([".png", ".jpg", ".jpeg", ".gif", ".webp"]);
234
+ function inferInputType(raw) {
235
+ if (raw === "-") return "stdin";
236
+ if (raw.startsWith("http://") || raw.startsWith("https://")) return "url";
237
+ if (IMAGE_EXTENSIONS.has(extname(raw).toLowerCase())) return "image";
238
+ return "file";
239
+ }
240
+ async function resolveInput(raw, typeOverride) {
241
+ const type = typeOverride ?? inferInputType(raw);
242
+ switch (type) {
243
+ case "file":
244
+ case "image": {
245
+ const absolutePath = resolve2(raw);
246
+ await access(absolutePath);
247
+ return { type, path: absolutePath, raw };
248
+ }
249
+ case "url": {
250
+ return { type, url: raw, raw };
251
+ }
252
+ case "stdin": {
253
+ const content = await readStdin();
254
+ if (!content.trim()) {
255
+ throw new InputError("No input received on stdin", 2);
256
+ }
257
+ return { type, content, raw };
258
+ }
259
+ }
260
+ }
261
+ async function readStdin() {
262
+ if (process.stdin.isTTY) {
263
+ throw new InputError("No input on stdin (not piped)", 2);
264
+ }
265
+ process.stderr.write("Reading from stdin...\n");
266
+ const chunks = [];
267
+ for await (const chunk of process.stdin) {
268
+ chunks.push(chunk);
269
+ }
270
+ return Buffer.concat(chunks).toString("utf-8");
271
+ }
272
+ var InputError = class extends Error {
273
+ constructor(message, exitCode) {
274
+ super(message);
275
+ this.exitCode = exitCode;
276
+ this.name = "InputError";
277
+ }
278
+ };
279
+
280
+ // src/input/handlers/text-file.ts
281
+ import { readFile as readFile2 } from "fs/promises";
282
+ async function handleTextFile(input) {
283
+ const content = await readFile2(input.path, "utf-8");
284
+ const text = [
285
+ "<transcript>",
286
+ content,
287
+ "</transcript>"
288
+ ].join("\n");
289
+ return { text, claudeFlags: [] };
290
+ }
291
+
292
+ // src/input/handlers/image-file.ts
293
+ async function handleImageFile(input) {
294
+ const absolutePath = input.path;
295
+ const text = `[Image attached: ${absolutePath}]`;
296
+ return {
297
+ text,
298
+ claudeFlags: ["--image", absolutePath]
299
+ };
300
+ }
301
+
302
+ // src/input/handlers/url.ts
303
+ var MAX_CONTENT_LENGTH = 5e4;
304
+ var FETCH_TIMEOUT_MS = 1e4;
305
+ async function handleUrl(input) {
306
+ const url = input.url;
307
+ const controller = new AbortController();
308
+ const timeout = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
309
+ let response;
310
+ try {
311
+ response = await fetch(url, {
312
+ signal: controller.signal,
313
+ headers: {
314
+ Accept: "text/html, text/plain, */*"
315
+ }
316
+ });
317
+ } catch (err) {
318
+ clearTimeout(timeout);
319
+ if (err instanceof Error && err.name === "AbortError") {
320
+ throw new InputError(`URL fetch timed out after 10s: ${url}`, 2);
321
+ }
322
+ throw new InputError(`Failed to fetch URL: ${url}`, 2);
323
+ } finally {
324
+ clearTimeout(timeout);
325
+ }
326
+ if (!response.ok) {
327
+ throw new InputError(`URL returned ${response.status}: ${url}`, 2);
328
+ }
329
+ const contentType = response.headers.get("content-type") ?? "";
330
+ if (!contentType.includes("text/") && !contentType.includes("application/json")) {
331
+ throw new InputError(`Unsupported content type: ${contentType}`, 2);
332
+ }
333
+ const rawContent = await response.text();
334
+ let content = rawContent.replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim();
335
+ let truncated = false;
336
+ if (content.length > MAX_CONTENT_LENGTH) {
337
+ content = content.slice(0, MAX_CONTENT_LENGTH);
338
+ truncated = true;
339
+ }
340
+ const parts = [
341
+ `<article source="${url}">`,
342
+ content,
343
+ ...truncated ? [`[Content truncated at ${MAX_CONTENT_LENGTH.toLocaleString()} characters]`] : [],
344
+ "</article>"
345
+ ];
346
+ return { text: parts.join("\n"), claudeFlags: [] };
347
+ }
348
+
349
+ // src/input/handlers/stdin.ts
350
+ async function handleStdin(input) {
351
+ const text = [
352
+ "<transcript>",
353
+ input.content,
354
+ "</transcript>"
355
+ ].join("\n");
356
+ return { text, claudeFlags: [] };
357
+ }
358
+
359
+ // src/input/prompt-builder.ts
360
+ async function buildPromptContent(input, context) {
361
+ let result;
362
+ switch (input.type) {
363
+ case "file":
364
+ result = await handleTextFile(input);
365
+ break;
366
+ case "image":
367
+ result = await handleImageFile(input);
368
+ break;
369
+ case "url":
370
+ result = await handleUrl(input);
371
+ break;
372
+ case "stdin":
373
+ result = await handleStdin(input);
374
+ break;
375
+ }
376
+ if (context) {
377
+ result = {
378
+ text: result.text + "\n\nAdditional instructions from the user:\n" + context,
379
+ claudeFlags: [...result.claudeFlags]
380
+ };
381
+ }
382
+ return result;
383
+ }
384
+
385
+ // src/prompts/shared.ts
386
+ function formatProjectContext(name, dirs) {
387
+ const dirList = dirs.map((d) => ` - ${d}`).join("\n");
388
+ return [
389
+ `<project>`,
390
+ `Name: ${name}`,
391
+ `Directories:`,
392
+ dirList,
393
+ `</project>`
394
+ ].join("\n");
395
+ }
396
+ function formatInputSection(content, type) {
397
+ if (!content || content.trim() === "") {
398
+ return [
399
+ `<context type="empty">`,
400
+ `The user wants to start a session for this project. Ask what they'd like to work on.`,
401
+ `</context>`
402
+ ].join("\n");
403
+ }
404
+ if (type === "image") {
405
+ return [
406
+ `<context type="image">`,
407
+ content,
408
+ `Analyze the attached image for tasks, bugs, or design changes.`,
409
+ `</context>`
410
+ ].join("\n");
411
+ }
412
+ return [
413
+ `<context type="${type}">`,
414
+ content,
415
+ `</context>`
416
+ ].join("\n");
417
+ }
418
+
419
+ // src/prompts/fire-and-forget.ts
420
+ function buildFireAndForgetPrompt(config) {
421
+ const { projectName, projectDirs, inputContent, inputType, context, joycraft } = config;
422
+ const projectBlock = formatProjectContext(projectName, projectDirs);
423
+ const inputBlock = formatInputSection(inputContent, inputType);
424
+ const joycraftInstructions = joycraft ? [
425
+ `Joycraft is available in this project. Use it to:`,
426
+ ` 1. Write atomic specs for each task you identify`,
427
+ ` 2. Implement each spec`,
428
+ ` 3. Open a PR with all changes`
429
+ ].join("\n") : [
430
+ `Make a plan for implementation, prioritize the tasks, implement them,`,
431
+ `and commit your work. Open a PR when done.`
432
+ ].join("\n");
433
+ const contextAddition = context ? [
434
+ ``,
435
+ `<additional-context>`,
436
+ `Additional instructions from the user:`,
437
+ context,
438
+ `</additional-context>`
439
+ ].join("\n") : "";
440
+ return [
441
+ projectBlock,
442
+ ``,
443
+ inputBlock,
444
+ contextAddition,
445
+ ``,
446
+ `<instructions>`,
447
+ `You are working on the "${projectName}" project. Analyze the context provided above`,
448
+ `and extract all relevant development tasks and action items.`,
449
+ ``,
450
+ joycraftInstructions,
451
+ ``,
452
+ `Work fully autonomously -- do not wait for user input. You have full authority`,
453
+ `to analyze, plan, implement, and open a PR.`,
454
+ ``,
455
+ `If critical information is missing, proceed with reasonable assumptions and note`,
456
+ `them in the PR description. Do not block on unanswered questions.`,
457
+ ``,
458
+ `Spin up teammates to execute tasks in parallel where possible.`,
459
+ `</instructions>`
460
+ ].join("\n");
461
+ }
462
+
463
+ // src/prompts/interactive.ts
464
+ function buildInteractivePrompt(config) {
465
+ const { projectName, projectDirs, inputContent, inputType, context, joycraft } = config;
466
+ const projectBlock = formatProjectContext(projectName, projectDirs);
467
+ const inputBlock = formatInputSection(inputContent, inputType);
468
+ const joycraftInstructions = joycraft ? [
469
+ `Joycraft is available in this project. Start by running /interview or /new-feature`,
470
+ `with the context below as starting material. Let the interview process guide`,
471
+ `the conversation before any implementation begins.`
472
+ ].join("\n") : [
473
+ `Start by having a conversation about the context above. Ask clarifying questions`,
474
+ `to understand the user's goals and priorities before implementing anything.`,
475
+ `Do not write code until the user confirms the plan.`
476
+ ].join("\n");
477
+ const contextAddition = context ? [
478
+ ``,
479
+ `<additional-context>`,
480
+ `Additional instructions from the user:`,
481
+ context,
482
+ `</additional-context>`
483
+ ].join("\n") : "";
484
+ return [
485
+ projectBlock,
486
+ ``,
487
+ inputBlock,
488
+ contextAddition,
489
+ ``,
490
+ `<instructions>`,
491
+ `You are working on the "${projectName}" project. The user has pipit'd the context`,
492
+ `above into this session for discussion and collaboration.`,
493
+ ``,
494
+ joycraftInstructions,
495
+ ``,
496
+ `Wait for the user to participate before implementing anything. This is a`,
497
+ `collaborative session -- explore the context together, clarify requirements,`,
498
+ `and agree on an approach before writing any code.`,
499
+ `</instructions>`
500
+ ].join("\n");
501
+ }
502
+
503
+ // src/joycraft/detector.ts
504
+ import { existsSync } from "fs";
505
+ import { join } from "path";
506
+ function detectJoycraft(projectDir) {
507
+ return existsSync(join(projectDir, ".claude", "skills"));
508
+ }
509
+
510
+ // src/input/types.ts
511
+ import { z as z2 } from "zod";
512
+ var InputTypeSchema = z2.enum(["file", "image", "url", "stdin"]);
513
+ var ControlLevelSchema = z2.enum(["auto", "plan_first", "extract_only"]);
514
+ var SessionModeSchema = z2.enum(["fire_and_forget", "interactive"]);
515
+ var JsonOutputSchema = z2.object({
516
+ success: z2.boolean(),
517
+ input_type: InputTypeSchema.optional(),
518
+ input_resolved: z2.string().optional(),
519
+ project_dir: z2.string().optional(),
520
+ terminal: z2.string().optional(),
521
+ prompt_version: z2.number().optional(),
522
+ error_code: z2.number().optional(),
523
+ error: z2.string().optional()
524
+ });
525
+ var EXIT = {
526
+ SUCCESS: 0,
527
+ GENERAL_ERROR: 1,
528
+ INPUT_NOT_FOUND: 2,
529
+ INVALID_ARGS: 3,
530
+ TERMINAL_LAUNCH_FAILED: 4
531
+ };
532
+
533
+ // src/cli.ts
534
+ var program = new Command();
535
+ program.name("pipit").description(
536
+ "Pipe any context into Claude Code sessions"
537
+ ).version("0.1.0");
538
+ function collect(value, previous) {
539
+ return previous.concat([value]);
540
+ }
541
+ program.command("process").description("Process a meeting transcript file").argument("<transcript-file>", "Path to the transcript file to process").option("-p, --project-dir <dir>", "Target project directory").option("-n, --dry-run", "Extract tasks without implementing them").option(
542
+ "-t, --task-index <index>",
543
+ "Only implement a specific task by index",
544
+ parseInt
545
+ ).action(
546
+ async (transcriptFile, options) => {
547
+ try {
548
+ const absoluteTranscriptPath = resolve3(transcriptFile);
549
+ try {
550
+ await access2(absoluteTranscriptPath);
551
+ } catch {
552
+ console.error(`Error: Transcript file not found: ${absoluteTranscriptPath}`);
553
+ process.exit(1);
554
+ }
555
+ const projectDir = resolve3(options.projectDir ?? process.cwd());
556
+ console.log(`Extracting tasks from ${absoluteTranscriptPath}...`);
557
+ const result = await extractTasks(absoluteTranscriptPath, projectDir);
558
+ console.log("");
559
+ console.log(`Meeting summary: ${result.meeting_summary}`);
560
+ console.log("");
561
+ console.log(`Action items found: ${result.action_items.length}`);
562
+ for (let i = 0; i < result.action_items.length; i++) {
563
+ const item = result.action_items[i];
564
+ console.log(
565
+ ` [${i}] [${item.priority}] [${item.estimated_complexity}] ${item.title}`
566
+ );
567
+ }
568
+ if (result.decisions.length > 0) {
569
+ console.log("");
570
+ console.log("Decisions made:");
571
+ for (const decision of result.decisions) {
572
+ console.log(` - ${decision}`);
573
+ }
574
+ }
575
+ if (result.questions_to_clarify.length > 0) {
576
+ console.log("");
577
+ console.log("Questions to clarify:");
578
+ for (const question of result.questions_to_clarify) {
579
+ console.log(` - ${question}`);
580
+ }
581
+ }
582
+ if (options.dryRun) {
583
+ console.log("");
584
+ console.log("Dry run -- skipping implementation");
585
+ return;
586
+ }
587
+ const taskIndex = options.taskIndex ?? 0;
588
+ if (taskIndex < 0 || taskIndex >= result.action_items.length) {
589
+ console.error(
590
+ `Error: Task index ${taskIndex} is out of range (0-${result.action_items.length - 1})`
591
+ );
592
+ process.exit(1);
593
+ }
594
+ const task = result.action_items[taskIndex];
595
+ console.log("");
596
+ console.log(`Implementing task: ${task.title}...`);
597
+ const taskResult = await implementTask({
598
+ task,
599
+ projectDir,
600
+ transcriptContext: task.transcript_context
601
+ });
602
+ console.log("");
603
+ if (taskResult.success) {
604
+ console.log("Claude Code session launched in a new terminal tab.");
605
+ console.log(` Branch: ${taskResult.branch_name}`);
606
+ console.log(` Worktree: ${taskResult.worktree_path}`);
607
+ console.log("");
608
+ console.log("When Claude finishes, you can:");
609
+ console.log(` cd ${taskResult.worktree_path}`);
610
+ console.log(` git push origin ${taskResult.branch_name} && gh pr create`);
611
+ } else {
612
+ console.error(`Failed to launch session: ${taskResult.error}`);
613
+ process.exit(1);
614
+ }
615
+ } catch (err) {
616
+ const message = err instanceof Error ? err.message : "Unknown error";
617
+ console.error(`Error: ${message}`);
618
+ process.exit(1);
619
+ }
620
+ }
621
+ );
622
+ program.command("go").description("Send any context to Claude Code -- let it extract, plan, and implement").argument("<input>", "Input: file path, URL (http/https), or - for stdin").argument("<project-name>", "Name of the project this context is for").option("-p, --project-dir <dir>", "Project directory (repeatable for multi-dir projects)", collect, []).addOption(
623
+ new Option("--input-type <type>", "Override input type inference").choices(["file", "image", "url", "stdin"])
624
+ ).addOption(
625
+ new Option("--control-level <level>", "Control level for Claude session").choices(["auto", "plan_first", "extract_only"]).default("auto")
626
+ ).option("--terminal <name>", "Terminal to use (ghostty, iterm2, warp, terminal)").option("--context <text>", "Additional instructions appended to the prompt").option("--dry-run", "Print the resolved prompt without launching", false).option("--json", "Emit machine-readable JSON to stdout", false).addOption(
627
+ new Option("--mode <mode>", "Session mode").choices(["fire_and_forget", "interactive"]).default("fire_and_forget")
628
+ ).action(
629
+ async (rawInput, projectName, opts) => {
630
+ const inputType = opts.inputType ? InputTypeSchema.parse(opts.inputType) : void 0;
631
+ const controlLevel = ControlLevelSchema.parse(opts.controlLevel);
632
+ const mode = SessionModeSchema.parse(opts.mode);
633
+ const projectDirs = opts.projectDir.length > 0 ? opts.projectDir.map((d) => resolve3(d)) : [resolve3(process.cwd())];
634
+ const options = {
635
+ projectDir: projectDirs,
636
+ inputType,
637
+ controlLevel,
638
+ terminal: opts.terminal,
639
+ context: opts.context,
640
+ dryRun: opts.dryRun,
641
+ json: opts.json,
642
+ mode
643
+ };
644
+ function outputJson(data) {
645
+ process.stdout.write(JSON.stringify(data) + "\n");
646
+ }
647
+ function fail(message, exitCode) {
648
+ if (options.json) {
649
+ outputJson({ success: false, error_code: exitCode, error: message });
650
+ } else {
651
+ console.error(`Error: ${message}`);
652
+ }
653
+ process.exit(exitCode);
654
+ }
655
+ try {
656
+ const resolved = await resolveInput(rawInput, inputType);
657
+ const promptContent = await buildPromptContent(resolved, options.context);
658
+ const joycraft = detectJoycraft(projectDirs[0]);
659
+ const promptConfig = {
660
+ projectName,
661
+ projectDirs,
662
+ inputContent: promptContent.text,
663
+ inputType: resolved.type,
664
+ context: options.context,
665
+ mode,
666
+ joycraft
667
+ };
668
+ const prompt = mode === "interactive" ? buildInteractivePrompt(promptConfig) : buildFireAndForgetPrompt(promptConfig);
669
+ if (options.dryRun) {
670
+ if (options.json) {
671
+ outputJson({
672
+ success: true,
673
+ input_type: resolved.type,
674
+ input_resolved: resolved.path ?? resolved.url ?? "(stdin)",
675
+ project_dir: projectDirs[0],
676
+ terminal: options.terminal ?? process.env.PIPIT_TERMINAL ?? "ghostty",
677
+ prompt_version: 2
678
+ });
679
+ } else {
680
+ console.log(prompt);
681
+ }
682
+ return;
683
+ }
684
+ const escapedPrompt = prompt.replace(/'/g, "'\\''");
685
+ const controlFlag = controlLevel === "auto" ? "--dangerously-skip-permissions" : "";
686
+ const extraFlags = promptContent.claudeFlags.map((f) => f.includes(" ") ? `'${f.replace(/'/g, "'\\''")}'` : f).join(" ");
687
+ const claudeCommand = `claude ${controlFlag} ${extraFlags} '${escapedPrompt}'`.replace(/\s+/g, " ").trim();
688
+ if (options.terminal) {
689
+ process.env.PIPIT_TERMINAL = options.terminal;
690
+ }
691
+ const workingDir = projectDirs[0];
692
+ if (!options.json) {
693
+ console.log(`Launching Claude Code session for ${projectName}...`);
694
+ }
695
+ try {
696
+ await launchClaudeTab(workingDir, claudeCommand);
697
+ } catch (err) {
698
+ const msg = err instanceof Error ? err.message : "Terminal launch failed";
699
+ fail(msg, EXIT.TERMINAL_LAUNCH_FAILED);
700
+ }
701
+ if (options.json) {
702
+ outputJson({
703
+ success: true,
704
+ input_type: resolved.type,
705
+ input_resolved: resolved.path ?? resolved.url ?? "(stdin)",
706
+ project_dir: workingDir,
707
+ terminal: options.terminal ?? process.env.PIPIT_TERMINAL ?? "ghostty",
708
+ prompt_version: 2
709
+ });
710
+ } else {
711
+ console.log("");
712
+ console.log("Claude Code session launched in a new terminal tab.");
713
+ console.log(` Project: ${workingDir}`);
714
+ if (projectDirs.length > 1) {
715
+ console.log(` Additional dirs: ${projectDirs.slice(1).join(", ")}`);
716
+ }
717
+ console.log("");
718
+ console.log("Claude will extract tasks, plan, and implement. Watch the tab!");
719
+ }
720
+ } catch (err) {
721
+ if (err instanceof InputError) {
722
+ fail(err.message, err.exitCode);
723
+ }
724
+ const message = err instanceof Error ? err.message : "Unknown error";
725
+ fail(message, EXIT.GENERAL_ERROR);
726
+ }
727
+ }
728
+ );
729
+ program.parse();
package/package.json ADDED
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "pipit-cli",
3
+ "version": "0.1.0",
4
+ "description": "Pipe any context into Claude Code sessions. Screenshots, meeting notes, URLs, clipboard -- capture it and let Claude handle the rest.",
5
+ "type": "module",
6
+ "bin": {
7
+ "pipit": "./dist/cli.js"
8
+ },
9
+ "files": [
10
+ "dist"
11
+ ],
12
+ "scripts": {
13
+ "build": "tsup",
14
+ "dev": "tsup --watch",
15
+ "test": "vitest",
16
+ "lint": "eslint .",
17
+ "typecheck": "tsc --noEmit",
18
+ "prepublishOnly": "pnpm build && pnpm test --run && pnpm typecheck"
19
+ },
20
+ "keywords": [
21
+ "claude",
22
+ "claude-code",
23
+ "ai",
24
+ "cli",
25
+ "transcript",
26
+ "meeting-notes",
27
+ "automation"
28
+ ],
29
+ "author": "Maximilian Maksutovic",
30
+ "license": "MIT",
31
+ "repository": {
32
+ "type": "git",
33
+ "url": "git+https://github.com/maksutovic/pipit.git"
34
+ },
35
+ "homepage": "https://github.com/maksutovic/pipit#readme",
36
+ "dependencies": {
37
+ "commander": "^13.1.0",
38
+ "execa": "^9.5.2",
39
+ "zod": "^3.24.2"
40
+ },
41
+ "devDependencies": {
42
+ "@types/node": "^22.13.10",
43
+ "@typescript-eslint/eslint-plugin": "^8.26.1",
44
+ "@typescript-eslint/parser": "^8.26.1",
45
+ "eslint": "^9.22.0",
46
+ "tsup": "^8.4.0",
47
+ "typescript": "^5.8.2",
48
+ "vitest": "^3.0.9"
49
+ },
50
+ "engines": {
51
+ "node": ">=20"
52
+ },
53
+ "packageManager": "pnpm@10.19.0"
54
+ }