@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 +6 -0
- package/dist/index.js +137 -1
- package/package.json +1 -1
- package/src/commands/status.ts +180 -0
- package/src/index.ts +9 -1
package/CHANGELOG.md
CHANGED
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(
|
|
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
|
@@ -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(
|
|
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();
|