@residue/cli 0.0.2 → 0.0.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/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # @residue/cli
2
2
 
3
+ ## 0.0.3
4
+
5
+ ### Patch Changes
6
+
7
+ - Add status command, common workflows docs, versioning policy.
8
+
3
9
  ## 0.0.2
4
10
 
5
11
  ### Patch Changes
package/dist/index.js CHANGED
@@ -4450,9 +4450,144 @@ function setup(opts) {
4450
4450
  });
4451
4451
  }
4452
4452
 
4453
+ // src/commands/status.ts
4454
+ import { readFile as readFile4, stat as stat5 } from "fs/promises";
4455
+ import { join as join6 } from "path";
4456
+ var log9 = createLogger("status");
4457
+ function checkFileExists(path) {
4458
+ return ResultAsync.fromPromise(stat5(path).then(() => true), toCliError({ message: "Failed to check file", code: "IO_ERROR" })).orElse(() => okAsync(false));
4459
+ }
4460
+ function checkHookInstalled(opts) {
4461
+ const hookPath = join6(opts.gitDir, "hooks", opts.hookName);
4462
+ return ResultAsync.fromPromise(readFile4(hookPath, "utf-8").then((content) => content.includes(opts.needle)), toCliError({ message: "Failed to read hook", code: "IO_ERROR" })).orElse(() => okAsync(false));
4463
+ }
4464
+ function getGitDir() {
4465
+ return ResultAsync.fromPromise((async () => {
4466
+ const proc = Bun.spawn(["git", "rev-parse", "--git-dir"], {
4467
+ stdout: "pipe",
4468
+ stderr: "pipe"
4469
+ });
4470
+ await proc.exited;
4471
+ return (await new Response(proc.stdout).text()).trim();
4472
+ })(), toCliError({ message: "Failed to get git directory", code: "GIT_ERROR" }));
4473
+ }
4474
+ function status() {
4475
+ return safeTry(async function* () {
4476
+ const isRepo = yield* isGitRepo();
4477
+ if (!isRepo) {
4478
+ log9.info("Not a git repository.");
4479
+ return ok(undefined);
4480
+ }
4481
+ const projectRoot = yield* getProjectRoot();
4482
+ log9.info("Login");
4483
+ const globalConfig = yield* readConfig();
4484
+ if (globalConfig) {
4485
+ log9.info(` global: ${globalConfig.worker_url}`);
4486
+ } else {
4487
+ log9.info(" global: not configured");
4488
+ }
4489
+ const localConfig = yield* readLocalConfig(projectRoot);
4490
+ if (localConfig) {
4491
+ log9.info(` local: ${localConfig.worker_url}`);
4492
+ } else {
4493
+ log9.info(" local: not configured");
4494
+ }
4495
+ const isActiveConfig = localConfig ?? globalConfig;
4496
+ if (isActiveConfig) {
4497
+ log9.info(` active: ${isActiveConfig.worker_url}`);
4498
+ } else {
4499
+ log9.info(' active: none (run "residue login" to configure)');
4500
+ }
4501
+ log9.info("");
4502
+ log9.info("Hooks");
4503
+ const gitDir = yield* getGitDir();
4504
+ const isPostCommitInstalled = yield* checkHookInstalled({
4505
+ gitDir,
4506
+ hookName: "post-commit",
4507
+ needle: "residue capture"
4508
+ });
4509
+ log9.info(` post-commit: ${isPostCommitInstalled ? "installed" : "not installed"}`);
4510
+ const isPrePushInstalled = yield* checkHookInstalled({
4511
+ gitDir,
4512
+ hookName: "pre-push",
4513
+ needle: "residue sync"
4514
+ });
4515
+ log9.info(` pre-push: ${isPrePushInstalled ? "installed" : "not installed"}`);
4516
+ if (!isPostCommitInstalled || !isPrePushInstalled) {
4517
+ log9.info(' run "residue init" to install missing hooks');
4518
+ }
4519
+ log9.info("");
4520
+ log9.info("Adapters");
4521
+ const isClaudeSetup = yield* checkFileExists(join6(projectRoot, ".claude", "settings.json"));
4522
+ let isClaudeHookConfigured = false;
4523
+ if (isClaudeSetup) {
4524
+ isClaudeHookConfigured = yield* ResultAsync.fromPromise(readFile4(join6(projectRoot, ".claude", "settings.json"), "utf-8").then((content) => content.includes("residue hook claude-code")), toCliError({
4525
+ message: "Failed to read claude settings",
4526
+ code: "IO_ERROR"
4527
+ })).orElse(() => okAsync(false));
4528
+ }
4529
+ log9.info(` claude-code: ${isClaudeHookConfigured ? "configured" : "not configured"}`);
4530
+ const isPiSetup = yield* checkFileExists(join6(projectRoot, ".pi", "extensions", "residue.ts"));
4531
+ log9.info(` pi: ${isPiSetup ? "configured" : "not configured"}`);
4532
+ log9.info("");
4533
+ log9.info("Sessions");
4534
+ const pendingPath = yield* getPendingPath(projectRoot);
4535
+ const sessions = yield* readPending(pendingPath);
4536
+ if (sessions.length === 0) {
4537
+ log9.info(" no pending sessions");
4538
+ } else {
4539
+ const openSessions = sessions.filter((s) => s.status === "open");
4540
+ const endedSessions = sessions.filter((s) => s.status === "ended");
4541
+ const totalCommits = sessions.reduce((sum, s) => sum + s.commits.length, 0);
4542
+ const sessionsWithCommits = sessions.filter((s) => s.commits.length > 0);
4543
+ log9.info(` total: ${sessions.length}`);
4544
+ log9.info(` open: ${openSessions.length}`);
4545
+ log9.info(` ended: ${endedSessions.length}`);
4546
+ log9.info(` commits: ${totalCommits} across ${sessionsWithCommits.length} session(s)`);
4547
+ const isReadyToSync = sessionsWithCommits.length > 0;
4548
+ if (isReadyToSync) {
4549
+ log9.info(` ${sessionsWithCommits.length} session(s) ready to sync on next push`);
4550
+ } else {
4551
+ log9.info(" no sessions ready to sync (no commits captured yet)");
4552
+ }
4553
+ }
4554
+ return ok(undefined);
4555
+ });
4556
+ }
4557
+ // package.json
4558
+ var package_default = {
4559
+ name: "@residue/cli",
4560
+ version: "0.0.3",
4561
+ repository: {
4562
+ type: "git",
4563
+ url: "https://github.com/butttons/residue",
4564
+ directory: "packages/cli"
4565
+ },
4566
+ type: "module",
4567
+ bin: {
4568
+ residue: "./dist/index.js"
4569
+ },
4570
+ scripts: {
4571
+ dev: "bun run src/index.ts",
4572
+ build: "bun build src/index.ts --outfile dist/index.js --target bun",
4573
+ test: "bun test",
4574
+ typecheck: "tsc --noEmit"
4575
+ },
4576
+ devDependencies: {
4577
+ "@types/bun": "latest",
4578
+ "@types/debug": "^4.1.12",
4579
+ typescript: "^5.7.0"
4580
+ },
4581
+ dependencies: {
4582
+ commander: "^14.0.3",
4583
+ debug: "^4.4.3",
4584
+ neverthrow: "^8.2.0"
4585
+ }
4586
+ };
4587
+
4453
4588
  // src/index.ts
4454
4589
  var program2 = new Command;
4455
- program2.name("residue").description("Capture AI agent conversations linked to git commits").version("0.0.1");
4590
+ program2.name("residue").description("Capture AI agent conversations linked to git commits").version(package_default.version);
4456
4591
  program2.command("login").description("Save worker URL and auth token").requiredOption("--url <worker_url>", "Worker URL").requiredOption("--token <auth_token>", "Auth token").option("--local", "Save config to this project instead of globally").action(wrapCommand((opts) => login({ url: opts.url, token: opts.token, isLocal: opts.local })));
4457
4592
  program2.command("init").description("Install git hooks in current repo").action(wrapCommand(() => init()));
4458
4593
  program2.command("setup").description("Configure an agent adapter for this project").argument("<agent>", "Agent to set up (claude-code, pi)").action(wrapCommand((agent) => setup({ agent })));
@@ -4468,4 +4603,5 @@ session.command("end").description("Mark an agent session as ended").requiredOpt
4468
4603
  program2.command("capture").description("Tag pending sessions with current commit SHA (called by post-commit hook)").action(wrapHookCommand(() => capture()));
4469
4604
  program2.command("sync").description("Upload pending sessions to worker (called by pre-push hook)").option("--remote-url <url>", "Remote URL (passed by pre-push hook)").action(wrapHookCommand((opts) => sync({ remoteUrl: opts.remoteUrl })));
4470
4605
  program2.command("push").description("Upload pending sessions to worker (manual trigger)").action(wrapCommand(() => push()));
4606
+ program2.command("status").description("Show current residue state for this project").action(wrapCommand(() => status()));
4471
4607
  program2.parse();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@residue/cli",
3
- "version": "0.0.2",
3
+ "version": "0.0.3",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/butttons/residue",
@@ -0,0 +1,180 @@
1
+ import { readFile, stat } from "fs/promises";
2
+ import { ok, okAsync, ResultAsync, safeTry } from "neverthrow";
3
+ import { join } from "path";
4
+ import { readConfig, readLocalConfig } from "@/lib/config";
5
+ import { isGitRepo } from "@/lib/git";
6
+ import { getPendingPath, getProjectRoot, readPending } from "@/lib/pending";
7
+ import type { CliError } from "@/utils/errors";
8
+ import { toCliError } from "@/utils/errors";
9
+ import { createLogger } from "@/utils/logger";
10
+
11
+ const log = createLogger("status");
12
+
13
+ function checkFileExists(path: string): ResultAsync<boolean, CliError> {
14
+ return ResultAsync.fromPromise(
15
+ stat(path).then(() => true),
16
+ toCliError({ message: "Failed to check file", code: "IO_ERROR" }),
17
+ ).orElse(() => okAsync(false));
18
+ }
19
+
20
+ function checkHookInstalled(opts: {
21
+ gitDir: string;
22
+ hookName: string;
23
+ needle: string;
24
+ }): ResultAsync<boolean, CliError> {
25
+ const hookPath = join(opts.gitDir, "hooks", opts.hookName);
26
+ return ResultAsync.fromPromise(
27
+ readFile(hookPath, "utf-8").then((content) =>
28
+ content.includes(opts.needle),
29
+ ),
30
+ toCliError({ message: "Failed to read hook", code: "IO_ERROR" }),
31
+ ).orElse(() => okAsync(false));
32
+ }
33
+
34
+ function getGitDir(): ResultAsync<string, CliError> {
35
+ return ResultAsync.fromPromise(
36
+ (async () => {
37
+ const proc = Bun.spawn(["git", "rev-parse", "--git-dir"], {
38
+ stdout: "pipe",
39
+ stderr: "pipe",
40
+ });
41
+ await proc.exited;
42
+ return (await new Response(proc.stdout).text()).trim();
43
+ })(),
44
+ toCliError({ message: "Failed to get git directory", code: "GIT_ERROR" }),
45
+ );
46
+ }
47
+
48
+ export function status(): ResultAsync<void, CliError> {
49
+ return safeTry(async function* () {
50
+ const isRepo = yield* isGitRepo();
51
+ if (!isRepo) {
52
+ log.info("Not a git repository.");
53
+ return ok(undefined);
54
+ }
55
+
56
+ const projectRoot = yield* getProjectRoot();
57
+
58
+ // -- Auth / Login state --
59
+ log.info("Login");
60
+
61
+ const globalConfig = yield* readConfig();
62
+ if (globalConfig) {
63
+ log.info(` global: ${globalConfig.worker_url}`);
64
+ } else {
65
+ log.info(" global: not configured");
66
+ }
67
+
68
+ const localConfig = yield* readLocalConfig(projectRoot);
69
+ if (localConfig) {
70
+ log.info(` local: ${localConfig.worker_url}`);
71
+ } else {
72
+ log.info(" local: not configured");
73
+ }
74
+
75
+ const isActiveConfig = localConfig ?? globalConfig;
76
+ if (isActiveConfig) {
77
+ log.info(` active: ${isActiveConfig.worker_url}`);
78
+ } else {
79
+ log.info(' active: none (run "residue login" to configure)');
80
+ }
81
+
82
+ log.info("");
83
+
84
+ // -- Git hooks --
85
+ log.info("Hooks");
86
+
87
+ const gitDir = yield* getGitDir();
88
+
89
+ const isPostCommitInstalled = yield* checkHookInstalled({
90
+ gitDir,
91
+ hookName: "post-commit",
92
+ needle: "residue capture",
93
+ });
94
+ log.info(
95
+ ` post-commit: ${isPostCommitInstalled ? "installed" : "not installed"}`,
96
+ );
97
+
98
+ const isPrePushInstalled = yield* checkHookInstalled({
99
+ gitDir,
100
+ hookName: "pre-push",
101
+ needle: "residue sync",
102
+ });
103
+ log.info(
104
+ ` pre-push: ${isPrePushInstalled ? "installed" : "not installed"}`,
105
+ );
106
+
107
+ if (!isPostCommitInstalled || !isPrePushInstalled) {
108
+ log.info(' run "residue init" to install missing hooks');
109
+ }
110
+
111
+ log.info("");
112
+
113
+ // -- Agent adapters --
114
+ log.info("Adapters");
115
+
116
+ const isClaudeSetup = yield* checkFileExists(
117
+ join(projectRoot, ".claude", "settings.json"),
118
+ );
119
+
120
+ let isClaudeHookConfigured = false;
121
+ if (isClaudeSetup) {
122
+ isClaudeHookConfigured = yield* ResultAsync.fromPromise(
123
+ readFile(join(projectRoot, ".claude", "settings.json"), "utf-8").then(
124
+ (content) => content.includes("residue hook claude-code"),
125
+ ),
126
+ toCliError({
127
+ message: "Failed to read claude settings",
128
+ code: "IO_ERROR",
129
+ }),
130
+ ).orElse(() => okAsync(false));
131
+ }
132
+
133
+ log.info(
134
+ ` claude-code: ${isClaudeHookConfigured ? "configured" : "not configured"}`,
135
+ );
136
+
137
+ const isPiSetup = yield* checkFileExists(
138
+ join(projectRoot, ".pi", "extensions", "residue.ts"),
139
+ );
140
+ log.info(` pi: ${isPiSetup ? "configured" : "not configured"}`);
141
+
142
+ log.info("");
143
+
144
+ // -- Pending sessions --
145
+ log.info("Sessions");
146
+
147
+ const pendingPath = yield* getPendingPath(projectRoot);
148
+ const sessions = yield* readPending(pendingPath);
149
+
150
+ if (sessions.length === 0) {
151
+ log.info(" no pending sessions");
152
+ } else {
153
+ const openSessions = sessions.filter((s) => s.status === "open");
154
+ const endedSessions = sessions.filter((s) => s.status === "ended");
155
+ const totalCommits = sessions.reduce(
156
+ (sum, s) => sum + s.commits.length,
157
+ 0,
158
+ );
159
+ const sessionsWithCommits = sessions.filter((s) => s.commits.length > 0);
160
+
161
+ log.info(` total: ${sessions.length}`);
162
+ log.info(` open: ${openSessions.length}`);
163
+ log.info(` ended: ${endedSessions.length}`);
164
+ log.info(
165
+ ` commits: ${totalCommits} across ${sessionsWithCommits.length} session(s)`,
166
+ );
167
+
168
+ const isReadyToSync = sessionsWithCommits.length > 0;
169
+ if (isReadyToSync) {
170
+ log.info(
171
+ ` ${sessionsWithCommits.length} session(s) ready to sync on next push`,
172
+ );
173
+ } else {
174
+ log.info(" no sessions ready to sync (no commits captured yet)");
175
+ }
176
+ }
177
+
178
+ return ok(undefined);
179
+ });
180
+ }
package/src/index.ts CHANGED
@@ -9,15 +9,18 @@ import { push } from "@/commands/push";
9
9
  import { sessionEnd } from "@/commands/session-end";
10
10
  import { sessionStart } from "@/commands/session-start";
11
11
  import { setup } from "@/commands/setup";
12
+ import { status } from "@/commands/status";
12
13
  import { sync } from "@/commands/sync";
13
14
  import { wrapCommand, wrapHookCommand } from "@/utils/errors";
14
15
 
16
+ import packageJson from "../package.json";
17
+
15
18
  const program = new Command();
16
19
 
17
20
  program
18
21
  .name("residue")
19
22
  .description("Capture AI agent conversations linked to git commits")
20
- .version("0.0.1");
23
+ .version(packageJson.version);
21
24
 
22
25
  program
23
26
  .command("login")
@@ -97,4 +100,9 @@ program
97
100
  .description("Upload pending sessions to worker (manual trigger)")
98
101
  .action(wrapCommand(() => push()));
99
102
 
103
+ program
104
+ .command("status")
105
+ .description("Show current residue state for this project")
106
+ .action(wrapCommand(() => status()));
107
+
100
108
  program.parse();