@residue/cli 0.0.3 → 0.0.4
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 +8 -0
- package/README.md +25 -1
- package/dist/index.js +442 -59
- package/package.json +1 -1
- package/src/commands/clear.ts +42 -0
- package/src/commands/search.ts +266 -0
- package/src/commands/sync.ts +85 -1
- package/src/index.ts +19 -0
- package/src/lib/search-text.ts +257 -0
- package/test/lib/search-text.test.ts +366 -0
package/dist/index.js
CHANGED
|
@@ -3622,10 +3622,40 @@ function capture() {
|
|
|
3622
3622
|
});
|
|
3623
3623
|
}
|
|
3624
3624
|
|
|
3625
|
+
// src/commands/clear.ts
|
|
3626
|
+
var log2 = createLogger("clear");
|
|
3627
|
+
function clear(opts) {
|
|
3628
|
+
return safeTry(async function* () {
|
|
3629
|
+
const projectRoot = yield* getProjectRoot();
|
|
3630
|
+
const pendingPath = yield* getPendingPath(projectRoot);
|
|
3631
|
+
const sessions = yield* readPending(pendingPath);
|
|
3632
|
+
if (sessions.length === 0) {
|
|
3633
|
+
log2.info("No pending sessions to clear.");
|
|
3634
|
+
return ok(undefined);
|
|
3635
|
+
}
|
|
3636
|
+
if (opts?.id) {
|
|
3637
|
+
const targetId = opts.id;
|
|
3638
|
+
const isFound = sessions.some((s) => s.id === targetId);
|
|
3639
|
+
if (!isFound) {
|
|
3640
|
+
log2.info(`Session ${targetId} not found in pending queue.`);
|
|
3641
|
+
return ok(undefined);
|
|
3642
|
+
}
|
|
3643
|
+
const remaining = sessions.filter((s) => s.id !== targetId);
|
|
3644
|
+
yield* writePending({ path: pendingPath, sessions: remaining });
|
|
3645
|
+
log2.info(`Cleared session ${targetId}.`);
|
|
3646
|
+
return ok(undefined);
|
|
3647
|
+
}
|
|
3648
|
+
const count = sessions.length;
|
|
3649
|
+
yield* writePending({ path: pendingPath, sessions: [] });
|
|
3650
|
+
log2.info(`Cleared ${count} pending session(s).`);
|
|
3651
|
+
return ok(undefined);
|
|
3652
|
+
});
|
|
3653
|
+
}
|
|
3654
|
+
|
|
3625
3655
|
// src/commands/hook.ts
|
|
3626
3656
|
import { mkdir as mkdir2, readFile, rm, stat, writeFile } from "fs/promises";
|
|
3627
3657
|
import { join as join2 } from "path";
|
|
3628
|
-
var
|
|
3658
|
+
var log3 = createLogger("hook");
|
|
3629
3659
|
function readStdin() {
|
|
3630
3660
|
return ResultAsync.fromPromise((async () => {
|
|
3631
3661
|
const chunks = [];
|
|
@@ -3722,7 +3752,7 @@ function handleSessionStart(opts) {
|
|
|
3722
3752
|
message: "Failed to write hook state file",
|
|
3723
3753
|
code: "IO_ERROR"
|
|
3724
3754
|
}));
|
|
3725
|
-
|
|
3755
|
+
log3.debug("session started for claude-code");
|
|
3726
3756
|
return ok(undefined);
|
|
3727
3757
|
});
|
|
3728
3758
|
}
|
|
@@ -3754,7 +3784,7 @@ function handleSessionEnd(opts) {
|
|
|
3754
3784
|
message: "Failed to remove hook state file",
|
|
3755
3785
|
code: "IO_ERROR"
|
|
3756
3786
|
}));
|
|
3757
|
-
|
|
3787
|
+
log3.debug("session %s ended", trimmedId);
|
|
3758
3788
|
return ok(undefined);
|
|
3759
3789
|
});
|
|
3760
3790
|
}
|
|
@@ -3785,7 +3815,7 @@ import {
|
|
|
3785
3815
|
writeFile as writeFile2
|
|
3786
3816
|
} from "fs/promises";
|
|
3787
3817
|
import { join as join3 } from "path";
|
|
3788
|
-
var
|
|
3818
|
+
var log4 = createLogger("init");
|
|
3789
3819
|
var POST_COMMIT_LINE = "residue capture >/dev/null 2>&1 &";
|
|
3790
3820
|
var PRE_PUSH_LINE = 'residue sync --remote-url "$2"';
|
|
3791
3821
|
function installHook(opts) {
|
|
@@ -3876,9 +3906,9 @@ function init() {
|
|
|
3876
3906
|
}),
|
|
3877
3907
|
ensureGitignore(projectRoot)
|
|
3878
3908
|
]);
|
|
3879
|
-
|
|
3880
|
-
|
|
3881
|
-
|
|
3909
|
+
log4.info("Initialized residue in this repository.");
|
|
3910
|
+
log4.info(` ${postCommit}`);
|
|
3911
|
+
log4.info(` ${prePush}`);
|
|
3882
3912
|
return ok(undefined);
|
|
3883
3913
|
});
|
|
3884
3914
|
}
|
|
@@ -3937,7 +3967,7 @@ function resolveConfig() {
|
|
|
3937
3967
|
}
|
|
3938
3968
|
|
|
3939
3969
|
// src/commands/login.ts
|
|
3940
|
-
var
|
|
3970
|
+
var log5 = createLogger("login");
|
|
3941
3971
|
function login(opts) {
|
|
3942
3972
|
if (!opts.url.startsWith("http://") && !opts.url.startsWith("https://")) {
|
|
3943
3973
|
return errAsync(new CliError({
|
|
@@ -3949,17 +3979,164 @@ function login(opts) {
|
|
|
3949
3979
|
const config = { worker_url: cleanUrl, token: opts.token };
|
|
3950
3980
|
if (opts.isLocal) {
|
|
3951
3981
|
return getProjectRoot().andThen((projectRoot) => writeLocalConfig({ projectRoot, config }).map(() => {
|
|
3952
|
-
|
|
3982
|
+
log5.info(`Logged in to ${cleanUrl} (project-local config)`);
|
|
3953
3983
|
}));
|
|
3954
3984
|
}
|
|
3955
3985
|
return writeConfig(config).map(() => {
|
|
3956
|
-
|
|
3986
|
+
log5.info(`Logged in to ${cleanUrl}`);
|
|
3957
3987
|
});
|
|
3958
3988
|
}
|
|
3959
3989
|
|
|
3990
|
+
// src/lib/search-text.ts
|
|
3991
|
+
function buildSearchText(opts) {
|
|
3992
|
+
const header = [
|
|
3993
|
+
`Session: ${opts.metadata.sessionId}`,
|
|
3994
|
+
`Agent: ${opts.metadata.agent}`,
|
|
3995
|
+
opts.metadata.commits.length > 0 ? `Commits: ${opts.metadata.commits.join(", ")}` : null,
|
|
3996
|
+
opts.metadata.branch ? `Branch: ${opts.metadata.branch}` : null,
|
|
3997
|
+
opts.metadata.repo ? `Repo: ${opts.metadata.repo}` : null
|
|
3998
|
+
].filter(Boolean).join(`
|
|
3999
|
+
`);
|
|
4000
|
+
const body = opts.lines.map((line) => `[${line.role}] ${line.text}`).join(`
|
|
4001
|
+
`);
|
|
4002
|
+
return `${header}
|
|
4003
|
+
|
|
4004
|
+
${body}
|
|
4005
|
+
`;
|
|
4006
|
+
}
|
|
4007
|
+
function extractClaudeCode(raw) {
|
|
4008
|
+
const lines = [];
|
|
4009
|
+
if (!raw.trim())
|
|
4010
|
+
return lines;
|
|
4011
|
+
const entries = [];
|
|
4012
|
+
for (const line of raw.split(`
|
|
4013
|
+
`)) {
|
|
4014
|
+
if (!line.trim())
|
|
4015
|
+
continue;
|
|
4016
|
+
try {
|
|
4017
|
+
entries.push(JSON.parse(line));
|
|
4018
|
+
} catch {}
|
|
4019
|
+
}
|
|
4020
|
+
for (const entry of entries) {
|
|
4021
|
+
if (entry.isMeta || entry.isSidechain)
|
|
4022
|
+
continue;
|
|
4023
|
+
if (entry.type === "user") {
|
|
4024
|
+
const content = entry.message?.content;
|
|
4025
|
+
if (!content)
|
|
4026
|
+
continue;
|
|
4027
|
+
if (typeof content === "string") {
|
|
4028
|
+
const trimmed = content.trim();
|
|
4029
|
+
if (trimmed)
|
|
4030
|
+
lines.push({ role: "human", text: trimmed });
|
|
4031
|
+
} else if (Array.isArray(content)) {
|
|
4032
|
+
const hasToolResult = content.some((b) => b.type === "tool_result");
|
|
4033
|
+
if (hasToolResult)
|
|
4034
|
+
continue;
|
|
4035
|
+
const text = content.filter((b) => b.type === "text" && b.text).map((b) => b.text).join(`
|
|
4036
|
+
`).trim();
|
|
4037
|
+
if (text)
|
|
4038
|
+
lines.push({ role: "human", text });
|
|
4039
|
+
}
|
|
4040
|
+
} else if (entry.type === "assistant") {
|
|
4041
|
+
const content = entry.message?.content;
|
|
4042
|
+
if (!Array.isArray(content))
|
|
4043
|
+
continue;
|
|
4044
|
+
for (const block of content) {
|
|
4045
|
+
if (block.type === "text" && block.text) {
|
|
4046
|
+
const trimmed = block.text.trim();
|
|
4047
|
+
if (trimmed)
|
|
4048
|
+
lines.push({ role: "assistant", text: trimmed });
|
|
4049
|
+
} else if (block.type === "tool_use" && block.name) {
|
|
4050
|
+
const desc = summarizeToolInput(block.name, block.input);
|
|
4051
|
+
lines.push({ role: "tool", text: desc });
|
|
4052
|
+
}
|
|
4053
|
+
}
|
|
4054
|
+
}
|
|
4055
|
+
}
|
|
4056
|
+
return lines;
|
|
4057
|
+
}
|
|
4058
|
+
function extractPi(raw) {
|
|
4059
|
+
const lines = [];
|
|
4060
|
+
if (!raw.trim())
|
|
4061
|
+
return lines;
|
|
4062
|
+
const entries = [];
|
|
4063
|
+
for (const line of raw.split(`
|
|
4064
|
+
`)) {
|
|
4065
|
+
if (!line.trim())
|
|
4066
|
+
continue;
|
|
4067
|
+
try {
|
|
4068
|
+
entries.push(JSON.parse(line));
|
|
4069
|
+
} catch {}
|
|
4070
|
+
}
|
|
4071
|
+
for (const entry of entries) {
|
|
4072
|
+
if (entry.type !== "message")
|
|
4073
|
+
continue;
|
|
4074
|
+
const msg = entry.message;
|
|
4075
|
+
if (!msg)
|
|
4076
|
+
continue;
|
|
4077
|
+
if (msg.role === "user") {
|
|
4078
|
+
const content = msg.content;
|
|
4079
|
+
if (!content)
|
|
4080
|
+
continue;
|
|
4081
|
+
if (typeof content === "string") {
|
|
4082
|
+
const trimmed = content.trim();
|
|
4083
|
+
if (trimmed)
|
|
4084
|
+
lines.push({ role: "human", text: trimmed });
|
|
4085
|
+
} else if (Array.isArray(content)) {
|
|
4086
|
+
const text = content.filter((b) => b.type === "text" && b.text).map((b) => b.text).join(`
|
|
4087
|
+
`).trim();
|
|
4088
|
+
if (text)
|
|
4089
|
+
lines.push({ role: "human", text });
|
|
4090
|
+
}
|
|
4091
|
+
} else if (msg.role === "assistant") {
|
|
4092
|
+
const content = msg.content;
|
|
4093
|
+
if (!Array.isArray(content)) {
|
|
4094
|
+
if (typeof content === "string" && content.trim()) {
|
|
4095
|
+
lines.push({ role: "assistant", text: content.trim() });
|
|
4096
|
+
}
|
|
4097
|
+
continue;
|
|
4098
|
+
}
|
|
4099
|
+
for (const block of content) {
|
|
4100
|
+
if (block.type === "text" && block.text) {
|
|
4101
|
+
const trimmed = block.text.trim();
|
|
4102
|
+
if (trimmed)
|
|
4103
|
+
lines.push({ role: "assistant", text: trimmed });
|
|
4104
|
+
} else if (block.type === "toolCall" && block.name) {
|
|
4105
|
+
const desc = summarizeToolInput(block.name, block.arguments);
|
|
4106
|
+
lines.push({ role: "tool", text: desc });
|
|
4107
|
+
}
|
|
4108
|
+
}
|
|
4109
|
+
}
|
|
4110
|
+
}
|
|
4111
|
+
return lines;
|
|
4112
|
+
}
|
|
4113
|
+
function summarizeToolInput(name, input) {
|
|
4114
|
+
if (!input)
|
|
4115
|
+
return name;
|
|
4116
|
+
const path = input.path ?? input.file_path ?? input.filePath ?? input.filename;
|
|
4117
|
+
if (typeof path === "string")
|
|
4118
|
+
return `${name} ${path}`;
|
|
4119
|
+
const command = input.command ?? input.cmd;
|
|
4120
|
+
if (typeof command === "string") {
|
|
4121
|
+
const short = command.length > 120 ? command.slice(0, 120) + "..." : command;
|
|
4122
|
+
return `${name} ${short}`;
|
|
4123
|
+
}
|
|
4124
|
+
const query = input.query ?? input.search ?? input.pattern;
|
|
4125
|
+
if (typeof query === "string")
|
|
4126
|
+
return `${name} ${query}`;
|
|
4127
|
+
return name;
|
|
4128
|
+
}
|
|
4129
|
+
var extractors = {
|
|
4130
|
+
"claude-code": extractClaudeCode,
|
|
4131
|
+
pi: extractPi
|
|
4132
|
+
};
|
|
4133
|
+
function getExtractor(agent) {
|
|
4134
|
+
return extractors[agent] ?? null;
|
|
4135
|
+
}
|
|
4136
|
+
|
|
3960
4137
|
// src/commands/sync.ts
|
|
3961
4138
|
import { stat as stat3 } from "fs/promises";
|
|
3962
|
-
var
|
|
4139
|
+
var log6 = createLogger("sync");
|
|
3963
4140
|
var STALE_THRESHOLD_MS = 30 * 60 * 1000;
|
|
3964
4141
|
function requestUploadUrl(opts) {
|
|
3965
4142
|
return ResultAsync.fromPromise(fetch(`${opts.workerUrl}/api/sessions/upload-url`, {
|
|
@@ -4022,7 +4199,7 @@ function buildCommitMeta(opts) {
|
|
|
4022
4199
|
for (const ref of opts.commitRefs) {
|
|
4023
4200
|
const metaResult = await getCommitMeta(ref.sha);
|
|
4024
4201
|
if (metaResult.isErr()) {
|
|
4025
|
-
|
|
4202
|
+
log6.warn(metaResult.error);
|
|
4026
4203
|
continue;
|
|
4027
4204
|
}
|
|
4028
4205
|
commits.push({
|
|
@@ -4050,17 +4227,54 @@ function closeStaleOpenSessions(opts) {
|
|
|
4050
4227
|
const checks = openSessions.map((session) => getFileMtimeMs(session.data_path).map((mtimeMs) => {
|
|
4051
4228
|
if (mtimeMs === null) {
|
|
4052
4229
|
session.status = "ended";
|
|
4053
|
-
|
|
4230
|
+
log6.debug("auto-closed session %s (data file not accessible)", session.id);
|
|
4054
4231
|
} else {
|
|
4055
4232
|
const msSinceModified = now - mtimeMs;
|
|
4056
4233
|
if (msSinceModified > STALE_THRESHOLD_MS) {
|
|
4057
4234
|
session.status = "ended";
|
|
4058
|
-
|
|
4235
|
+
log6.debug("auto-closed stale session %s (data file unchanged for %dm)", session.id, Math.round(msSinceModified / 60000));
|
|
4059
4236
|
}
|
|
4060
4237
|
}
|
|
4061
4238
|
}));
|
|
4062
4239
|
return ResultAsync.combine(checks).map(() => opts.sessions);
|
|
4063
4240
|
}
|
|
4241
|
+
function generateSearchText(opts) {
|
|
4242
|
+
const extractor = getExtractor(opts.session.agent);
|
|
4243
|
+
if (!extractor) {
|
|
4244
|
+
log6.debug("no search text extractor for agent %s, skipping", opts.session.agent);
|
|
4245
|
+
return null;
|
|
4246
|
+
}
|
|
4247
|
+
const searchLines = extractor(opts.rawData);
|
|
4248
|
+
if (searchLines.length === 0)
|
|
4249
|
+
return null;
|
|
4250
|
+
const branches = [
|
|
4251
|
+
...new Set(opts.session.commits.map((c) => c.branch).filter(Boolean))
|
|
4252
|
+
];
|
|
4253
|
+
return buildSearchText({
|
|
4254
|
+
metadata: {
|
|
4255
|
+
sessionId: opts.session.id,
|
|
4256
|
+
agent: opts.session.agent,
|
|
4257
|
+
commits: opts.commits.map((c) => c.sha.slice(0, 7)),
|
|
4258
|
+
branch: branches[0] ?? "",
|
|
4259
|
+
repo: `${opts.org}/${opts.repo}`
|
|
4260
|
+
},
|
|
4261
|
+
lines: searchLines
|
|
4262
|
+
});
|
|
4263
|
+
}
|
|
4264
|
+
function uploadSearchText(opts) {
|
|
4265
|
+
return ResultAsync.fromPromise(fetch(opts.url, {
|
|
4266
|
+
method: "PUT",
|
|
4267
|
+
headers: { "Content-Type": "text/plain" },
|
|
4268
|
+
body: opts.data
|
|
4269
|
+
}).then((response) => {
|
|
4270
|
+
if (!response.ok) {
|
|
4271
|
+
throw new Error(`R2 search upload failed: HTTP ${response.status}`);
|
|
4272
|
+
}
|
|
4273
|
+
}), toCliError({
|
|
4274
|
+
message: "Search text R2 upload failed",
|
|
4275
|
+
code: "NETWORK_ERROR"
|
|
4276
|
+
}));
|
|
4277
|
+
}
|
|
4064
4278
|
function syncSessions(opts) {
|
|
4065
4279
|
return ResultAsync.fromSafePromise((async () => {
|
|
4066
4280
|
const remaining = [];
|
|
@@ -4071,13 +4285,13 @@ function syncSessions(opts) {
|
|
|
4071
4285
|
}
|
|
4072
4286
|
const dataResult = await readSessionData(session.data_path);
|
|
4073
4287
|
if (dataResult.isErr()) {
|
|
4074
|
-
|
|
4288
|
+
log6.warn(dataResult.error);
|
|
4075
4289
|
remaining.push(session);
|
|
4076
4290
|
continue;
|
|
4077
4291
|
}
|
|
4078
4292
|
const data = dataResult.value;
|
|
4079
4293
|
if (data === null) {
|
|
4080
|
-
|
|
4294
|
+
log6.warn(`dropping session ${session.id}: data file missing at ${session.data_path}`);
|
|
4081
4295
|
continue;
|
|
4082
4296
|
}
|
|
4083
4297
|
const commitsResult = await buildCommitMeta({
|
|
@@ -4086,7 +4300,7 @@ function syncSessions(opts) {
|
|
|
4086
4300
|
repo: opts.repo
|
|
4087
4301
|
});
|
|
4088
4302
|
if (commitsResult.isErr()) {
|
|
4089
|
-
|
|
4303
|
+
log6.warn(commitsResult.error);
|
|
4090
4304
|
remaining.push(session);
|
|
4091
4305
|
continue;
|
|
4092
4306
|
}
|
|
@@ -4096,7 +4310,7 @@ function syncSessions(opts) {
|
|
|
4096
4310
|
sessionId: session.id
|
|
4097
4311
|
});
|
|
4098
4312
|
if (uploadUrlResult.isErr()) {
|
|
4099
|
-
|
|
4313
|
+
log6.warn(`failed to get upload URL for session ${session.id}: ${uploadUrlResult.error.message}`);
|
|
4100
4314
|
remaining.push(session);
|
|
4101
4315
|
continue;
|
|
4102
4316
|
}
|
|
@@ -4105,11 +4319,29 @@ function syncSessions(opts) {
|
|
|
4105
4319
|
data
|
|
4106
4320
|
});
|
|
4107
4321
|
if (directUploadResult.isErr()) {
|
|
4108
|
-
|
|
4322
|
+
log6.warn(`R2 upload failed for session ${session.id}: ${directUploadResult.error.message}`);
|
|
4109
4323
|
remaining.push(session);
|
|
4110
4324
|
continue;
|
|
4111
4325
|
}
|
|
4112
|
-
|
|
4326
|
+
log6.debug("uploaded session %s data directly to R2", session.id);
|
|
4327
|
+
const searchText = generateSearchText({
|
|
4328
|
+
session,
|
|
4329
|
+
rawData: data,
|
|
4330
|
+
commits: commitsResult.value,
|
|
4331
|
+
org: opts.org,
|
|
4332
|
+
repo: opts.repo
|
|
4333
|
+
});
|
|
4334
|
+
if (searchText && uploadUrlResult.value.search_url) {
|
|
4335
|
+
const searchUploadResult = await uploadSearchText({
|
|
4336
|
+
url: uploadUrlResult.value.search_url,
|
|
4337
|
+
data: searchText
|
|
4338
|
+
});
|
|
4339
|
+
if (searchUploadResult.isErr()) {
|
|
4340
|
+
log6.warn(`search text upload failed for session ${session.id}: ${searchUploadResult.error.message}`);
|
|
4341
|
+
} else {
|
|
4342
|
+
log6.debug("uploaded search text for session %s", session.id);
|
|
4343
|
+
}
|
|
4344
|
+
}
|
|
4113
4345
|
const metadataResult = await postSessionMetadata({
|
|
4114
4346
|
workerUrl: opts.workerUrl,
|
|
4115
4347
|
token: opts.token,
|
|
@@ -4122,14 +4354,14 @@ function syncSessions(opts) {
|
|
|
4122
4354
|
commits: commitsResult.value
|
|
4123
4355
|
});
|
|
4124
4356
|
if (metadataResult.isErr()) {
|
|
4125
|
-
|
|
4357
|
+
log6.warn(`metadata upload failed for session ${session.id}: ${metadataResult.error.message}`);
|
|
4126
4358
|
remaining.push(session);
|
|
4127
4359
|
continue;
|
|
4128
4360
|
}
|
|
4129
4361
|
if (session.status === "open") {
|
|
4130
4362
|
remaining.push(session);
|
|
4131
4363
|
}
|
|
4132
|
-
|
|
4364
|
+
log6.debug("synced session %s", session.id);
|
|
4133
4365
|
}
|
|
4134
4366
|
return remaining;
|
|
4135
4367
|
})());
|
|
@@ -4175,8 +4407,157 @@ function sync(opts) {
|
|
|
4175
4407
|
// src/commands/push.ts
|
|
4176
4408
|
var push = sync;
|
|
4177
4409
|
|
|
4410
|
+
// src/commands/search.ts
|
|
4411
|
+
var log7 = createLogger("search");
|
|
4412
|
+
function fetchSearch(opts) {
|
|
4413
|
+
const path = opts.isAi ? "/api/search/ai" : "/api/search";
|
|
4414
|
+
const url = `${opts.workerUrl}${path}?q=${encodeURIComponent(opts.query)}`;
|
|
4415
|
+
return ResultAsync.fromPromise(fetch(url, {
|
|
4416
|
+
headers: { Authorization: `Bearer ${opts.token}` }
|
|
4417
|
+
}).then(async (response) => {
|
|
4418
|
+
if (!response.ok) {
|
|
4419
|
+
const body = await response.text().catch(() => "");
|
|
4420
|
+
throw new Error(`HTTP ${response.status}: ${body}`);
|
|
4421
|
+
}
|
|
4422
|
+
return response.json();
|
|
4423
|
+
}), toCliError({ message: "Search request failed", code: "NETWORK_ERROR" }));
|
|
4424
|
+
}
|
|
4425
|
+
function fetchSessionCommits(opts) {
|
|
4426
|
+
const url = `${opts.workerUrl}/api/sessions/${opts.sessionId}/commits`;
|
|
4427
|
+
return ResultAsync.fromPromise(fetch(url, {
|
|
4428
|
+
headers: { Authorization: `Bearer ${opts.token}` }
|
|
4429
|
+
}).then(async (response) => {
|
|
4430
|
+
if (!response.ok)
|
|
4431
|
+
return [];
|
|
4432
|
+
const data = await response.json();
|
|
4433
|
+
return data.commits;
|
|
4434
|
+
}), toCliError({
|
|
4435
|
+
message: "Failed to fetch session commits",
|
|
4436
|
+
code: "NETWORK_ERROR"
|
|
4437
|
+
})).orElse(() => ok([]));
|
|
4438
|
+
}
|
|
4439
|
+
function extractSessionId(filename) {
|
|
4440
|
+
const match = filename.match(/(?:sessions|search)\/(.+?)\.(?:json|txt)$/);
|
|
4441
|
+
return match ? match[1] : filename;
|
|
4442
|
+
}
|
|
4443
|
+
function truncate(opts) {
|
|
4444
|
+
if (opts.text.length <= opts.maxLength)
|
|
4445
|
+
return opts.text;
|
|
4446
|
+
return opts.text.slice(0, opts.maxLength) + "...";
|
|
4447
|
+
}
|
|
4448
|
+
function formatSnippet(text) {
|
|
4449
|
+
const cleaned = text.replace(/\\n/g, " ").replace(/\\"/g, '"').replace(/\s+/g, " ").trim();
|
|
4450
|
+
return truncate({ text: cleaned, maxLength: 200 });
|
|
4451
|
+
}
|
|
4452
|
+
function buildCommitUrl(opts) {
|
|
4453
|
+
return `${opts.workerUrl}/app/${opts.org}/${opts.repo}/${opts.sha}`;
|
|
4454
|
+
}
|
|
4455
|
+
function renderSearchResults(opts) {
|
|
4456
|
+
if (opts.results.data.length === 0) {
|
|
4457
|
+
log7.info("No results found.");
|
|
4458
|
+
return;
|
|
4459
|
+
}
|
|
4460
|
+
log7.info(`${opts.results.data.length} result(s) for "${opts.results.search_query}"
|
|
4461
|
+
`);
|
|
4462
|
+
for (const item of opts.results.data) {
|
|
4463
|
+
const sessionId = extractSessionId(item.filename);
|
|
4464
|
+
const scorePercent = (item.score * 100).toFixed(1);
|
|
4465
|
+
log7.info(` ${sessionId} [${scorePercent}%]`);
|
|
4466
|
+
const snippet = item.content[0]?.text;
|
|
4467
|
+
if (snippet) {
|
|
4468
|
+
log7.info(` ${formatSnippet(snippet)}`);
|
|
4469
|
+
}
|
|
4470
|
+
const commits = opts.commitMap.get(sessionId) ?? [];
|
|
4471
|
+
if (commits.length > 0) {
|
|
4472
|
+
for (const commit of commits) {
|
|
4473
|
+
const url = buildCommitUrl({
|
|
4474
|
+
workerUrl: opts.workerUrl,
|
|
4475
|
+
org: commit.org,
|
|
4476
|
+
repo: commit.repo,
|
|
4477
|
+
sha: commit.commit_sha
|
|
4478
|
+
});
|
|
4479
|
+
log7.info(` -> ${url}`);
|
|
4480
|
+
}
|
|
4481
|
+
}
|
|
4482
|
+
log7.info("");
|
|
4483
|
+
}
|
|
4484
|
+
}
|
|
4485
|
+
function renderAiSearchResults(opts) {
|
|
4486
|
+
if (opts.results.response) {
|
|
4487
|
+
log7.info(opts.results.response);
|
|
4488
|
+
log7.info("");
|
|
4489
|
+
}
|
|
4490
|
+
if (opts.results.data.length > 0) {
|
|
4491
|
+
log7.info(`--- Sources (${opts.results.data.length}) ---
|
|
4492
|
+
`);
|
|
4493
|
+
for (const item of opts.results.data) {
|
|
4494
|
+
const sessionId = extractSessionId(item.filename);
|
|
4495
|
+
const scorePercent = (item.score * 100).toFixed(1);
|
|
4496
|
+
log7.info(` ${sessionId} [${scorePercent}%]`);
|
|
4497
|
+
const commits = opts.commitMap.get(sessionId) ?? [];
|
|
4498
|
+
if (commits.length > 0) {
|
|
4499
|
+
for (const commit of commits) {
|
|
4500
|
+
const url = buildCommitUrl({
|
|
4501
|
+
workerUrl: opts.workerUrl,
|
|
4502
|
+
org: commit.org,
|
|
4503
|
+
repo: commit.repo,
|
|
4504
|
+
sha: commit.commit_sha
|
|
4505
|
+
});
|
|
4506
|
+
log7.info(` -> ${url}`);
|
|
4507
|
+
}
|
|
4508
|
+
}
|
|
4509
|
+
}
|
|
4510
|
+
log7.info("");
|
|
4511
|
+
}
|
|
4512
|
+
}
|
|
4513
|
+
function isAiSearchResponse(response) {
|
|
4514
|
+
return "response" in response;
|
|
4515
|
+
}
|
|
4516
|
+
function search(opts) {
|
|
4517
|
+
return safeTry(async function* () {
|
|
4518
|
+
const config = yield* resolveConfig();
|
|
4519
|
+
if (!config) {
|
|
4520
|
+
return err(new CliError({
|
|
4521
|
+
message: "Not configured. Run 'residue login' first.",
|
|
4522
|
+
code: "CONFIG_MISSING"
|
|
4523
|
+
}));
|
|
4524
|
+
}
|
|
4525
|
+
const results = yield* fetchSearch({
|
|
4526
|
+
workerUrl: config.worker_url,
|
|
4527
|
+
token: config.token,
|
|
4528
|
+
query: opts.query,
|
|
4529
|
+
isAi: opts.isAi ?? false
|
|
4530
|
+
});
|
|
4531
|
+
const sessionIds = results.data.map((item) => extractSessionId(item.filename));
|
|
4532
|
+
const uniqueSessionIds = [...new Set(sessionIds)];
|
|
4533
|
+
const commitResults = yield* ResultAsync.combine(uniqueSessionIds.map((sessionId) => fetchSessionCommits({
|
|
4534
|
+
workerUrl: config.worker_url,
|
|
4535
|
+
token: config.token,
|
|
4536
|
+
sessionId
|
|
4537
|
+
}).map((commits) => ({ sessionId, commits }))));
|
|
4538
|
+
const commitMap = new Map;
|
|
4539
|
+
for (const entry of commitResults) {
|
|
4540
|
+
commitMap.set(entry.sessionId, entry.commits);
|
|
4541
|
+
}
|
|
4542
|
+
if (opts.isAi && isAiSearchResponse(results)) {
|
|
4543
|
+
renderAiSearchResults({
|
|
4544
|
+
results,
|
|
4545
|
+
commitMap,
|
|
4546
|
+
workerUrl: config.worker_url
|
|
4547
|
+
});
|
|
4548
|
+
} else {
|
|
4549
|
+
renderSearchResults({
|
|
4550
|
+
results,
|
|
4551
|
+
commitMap,
|
|
4552
|
+
workerUrl: config.worker_url
|
|
4553
|
+
});
|
|
4554
|
+
}
|
|
4555
|
+
return ok(undefined);
|
|
4556
|
+
});
|
|
4557
|
+
}
|
|
4558
|
+
|
|
4178
4559
|
// src/commands/session-end.ts
|
|
4179
|
-
var
|
|
4560
|
+
var log8 = createLogger("session");
|
|
4180
4561
|
function sessionEnd(opts) {
|
|
4181
4562
|
return safeTry(async function* () {
|
|
4182
4563
|
const projectRoot = yield* getProjectRoot();
|
|
@@ -4193,13 +4574,13 @@ function sessionEnd(opts) {
|
|
|
4193
4574
|
id: opts.id,
|
|
4194
4575
|
updates: { status: "ended" }
|
|
4195
4576
|
});
|
|
4196
|
-
|
|
4577
|
+
log8.debug("session %s ended", opts.id);
|
|
4197
4578
|
return ok(undefined);
|
|
4198
4579
|
});
|
|
4199
4580
|
}
|
|
4200
4581
|
|
|
4201
4582
|
// src/commands/session-start.ts
|
|
4202
|
-
var
|
|
4583
|
+
var log9 = createLogger("session");
|
|
4203
4584
|
function sessionStart(opts) {
|
|
4204
4585
|
const id = crypto.randomUUID();
|
|
4205
4586
|
return getProjectRoot().andThen(getPendingPath).andThen((pendingPath) => addSession({
|
|
@@ -4214,7 +4595,7 @@ function sessionStart(opts) {
|
|
|
4214
4595
|
}
|
|
4215
4596
|
})).map(() => {
|
|
4216
4597
|
process.stdout.write(id);
|
|
4217
|
-
|
|
4598
|
+
log9.debug("session started for %s", opts.agent);
|
|
4218
4599
|
});
|
|
4219
4600
|
}
|
|
4220
4601
|
|
|
@@ -4363,7 +4744,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
4363
4744
|
`;
|
|
4364
4745
|
|
|
4365
4746
|
// src/commands/setup.ts
|
|
4366
|
-
var
|
|
4747
|
+
var log10 = createLogger("setup");
|
|
4367
4748
|
var CLAUDE_HOOK_COMMAND = "residue hook claude-code";
|
|
4368
4749
|
function hasResidueHook(entries) {
|
|
4369
4750
|
return entries.some((entry) => entry.hooks.some((h) => h.command === CLAUDE_HOOK_COMMAND));
|
|
@@ -4408,12 +4789,12 @@ function setupClaudeCode(projectRoot) {
|
|
|
4408
4789
|
isChanged = true;
|
|
4409
4790
|
}
|
|
4410
4791
|
if (!isChanged) {
|
|
4411
|
-
|
|
4792
|
+
log10.info("residue hooks already configured in .claude/settings.json");
|
|
4412
4793
|
return;
|
|
4413
4794
|
}
|
|
4414
4795
|
await writeFile3(settingsPath, JSON.stringify(settings, null, 2) + `
|
|
4415
4796
|
`);
|
|
4416
|
-
|
|
4797
|
+
log10.info("Configured Claude Code hooks in .claude/settings.json");
|
|
4417
4798
|
})(), toCliError({ message: "Failed to setup Claude Code", code: "IO_ERROR" }));
|
|
4418
4799
|
}
|
|
4419
4800
|
function setupPi(projectRoot) {
|
|
@@ -4427,11 +4808,11 @@ function setupPi(projectRoot) {
|
|
|
4427
4808
|
isExisting = true;
|
|
4428
4809
|
} catch {}
|
|
4429
4810
|
if (isExisting) {
|
|
4430
|
-
|
|
4811
|
+
log10.info("residue extension already exists at .pi/extensions/residue.ts");
|
|
4431
4812
|
return;
|
|
4432
4813
|
}
|
|
4433
4814
|
await writeFile3(targetPath, extension_ts_default);
|
|
4434
|
-
|
|
4815
|
+
log10.info("Installed pi extension at .pi/extensions/residue.ts");
|
|
4435
4816
|
})(), toCliError({ message: "Failed to setup pi", code: "IO_ERROR" }));
|
|
4436
4817
|
}
|
|
4437
4818
|
function setup(opts) {
|
|
@@ -4453,7 +4834,7 @@ function setup(opts) {
|
|
|
4453
4834
|
// src/commands/status.ts
|
|
4454
4835
|
import { readFile as readFile4, stat as stat5 } from "fs/promises";
|
|
4455
4836
|
import { join as join6 } from "path";
|
|
4456
|
-
var
|
|
4837
|
+
var log11 = createLogger("status");
|
|
4457
4838
|
function checkFileExists(path) {
|
|
4458
4839
|
return ResultAsync.fromPromise(stat5(path).then(() => true), toCliError({ message: "Failed to check file", code: "IO_ERROR" })).orElse(() => okAsync(false));
|
|
4459
4840
|
}
|
|
@@ -4475,49 +4856,49 @@ function status() {
|
|
|
4475
4856
|
return safeTry(async function* () {
|
|
4476
4857
|
const isRepo = yield* isGitRepo();
|
|
4477
4858
|
if (!isRepo) {
|
|
4478
|
-
|
|
4859
|
+
log11.info("Not a git repository.");
|
|
4479
4860
|
return ok(undefined);
|
|
4480
4861
|
}
|
|
4481
4862
|
const projectRoot = yield* getProjectRoot();
|
|
4482
|
-
|
|
4863
|
+
log11.info("Login");
|
|
4483
4864
|
const globalConfig = yield* readConfig();
|
|
4484
4865
|
if (globalConfig) {
|
|
4485
|
-
|
|
4866
|
+
log11.info(` global: ${globalConfig.worker_url}`);
|
|
4486
4867
|
} else {
|
|
4487
|
-
|
|
4868
|
+
log11.info(" global: not configured");
|
|
4488
4869
|
}
|
|
4489
4870
|
const localConfig = yield* readLocalConfig(projectRoot);
|
|
4490
4871
|
if (localConfig) {
|
|
4491
|
-
|
|
4872
|
+
log11.info(` local: ${localConfig.worker_url}`);
|
|
4492
4873
|
} else {
|
|
4493
|
-
|
|
4874
|
+
log11.info(" local: not configured");
|
|
4494
4875
|
}
|
|
4495
4876
|
const isActiveConfig = localConfig ?? globalConfig;
|
|
4496
4877
|
if (isActiveConfig) {
|
|
4497
|
-
|
|
4878
|
+
log11.info(` active: ${isActiveConfig.worker_url}`);
|
|
4498
4879
|
} else {
|
|
4499
|
-
|
|
4880
|
+
log11.info(' active: none (run "residue login" to configure)');
|
|
4500
4881
|
}
|
|
4501
|
-
|
|
4502
|
-
|
|
4882
|
+
log11.info("");
|
|
4883
|
+
log11.info("Hooks");
|
|
4503
4884
|
const gitDir = yield* getGitDir();
|
|
4504
4885
|
const isPostCommitInstalled = yield* checkHookInstalled({
|
|
4505
4886
|
gitDir,
|
|
4506
4887
|
hookName: "post-commit",
|
|
4507
4888
|
needle: "residue capture"
|
|
4508
4889
|
});
|
|
4509
|
-
|
|
4890
|
+
log11.info(` post-commit: ${isPostCommitInstalled ? "installed" : "not installed"}`);
|
|
4510
4891
|
const isPrePushInstalled = yield* checkHookInstalled({
|
|
4511
4892
|
gitDir,
|
|
4512
4893
|
hookName: "pre-push",
|
|
4513
4894
|
needle: "residue sync"
|
|
4514
4895
|
});
|
|
4515
|
-
|
|
4896
|
+
log11.info(` pre-push: ${isPrePushInstalled ? "installed" : "not installed"}`);
|
|
4516
4897
|
if (!isPostCommitInstalled || !isPrePushInstalled) {
|
|
4517
|
-
|
|
4898
|
+
log11.info(' run "residue init" to install missing hooks');
|
|
4518
4899
|
}
|
|
4519
|
-
|
|
4520
|
-
|
|
4900
|
+
log11.info("");
|
|
4901
|
+
log11.info("Adapters");
|
|
4521
4902
|
const isClaudeSetup = yield* checkFileExists(join6(projectRoot, ".claude", "settings.json"));
|
|
4522
4903
|
let isClaudeHookConfigured = false;
|
|
4523
4904
|
if (isClaudeSetup) {
|
|
@@ -4526,29 +4907,29 @@ function status() {
|
|
|
4526
4907
|
code: "IO_ERROR"
|
|
4527
4908
|
})).orElse(() => okAsync(false));
|
|
4528
4909
|
}
|
|
4529
|
-
|
|
4910
|
+
log11.info(` claude-code: ${isClaudeHookConfigured ? "configured" : "not configured"}`);
|
|
4530
4911
|
const isPiSetup = yield* checkFileExists(join6(projectRoot, ".pi", "extensions", "residue.ts"));
|
|
4531
|
-
|
|
4532
|
-
|
|
4533
|
-
|
|
4912
|
+
log11.info(` pi: ${isPiSetup ? "configured" : "not configured"}`);
|
|
4913
|
+
log11.info("");
|
|
4914
|
+
log11.info("Sessions");
|
|
4534
4915
|
const pendingPath = yield* getPendingPath(projectRoot);
|
|
4535
4916
|
const sessions = yield* readPending(pendingPath);
|
|
4536
4917
|
if (sessions.length === 0) {
|
|
4537
|
-
|
|
4918
|
+
log11.info(" no pending sessions");
|
|
4538
4919
|
} else {
|
|
4539
4920
|
const openSessions = sessions.filter((s) => s.status === "open");
|
|
4540
4921
|
const endedSessions = sessions.filter((s) => s.status === "ended");
|
|
4541
4922
|
const totalCommits = sessions.reduce((sum, s) => sum + s.commits.length, 0);
|
|
4542
4923
|
const sessionsWithCommits = sessions.filter((s) => s.commits.length > 0);
|
|
4543
|
-
|
|
4544
|
-
|
|
4545
|
-
|
|
4546
|
-
|
|
4924
|
+
log11.info(` total: ${sessions.length}`);
|
|
4925
|
+
log11.info(` open: ${openSessions.length}`);
|
|
4926
|
+
log11.info(` ended: ${endedSessions.length}`);
|
|
4927
|
+
log11.info(` commits: ${totalCommits} across ${sessionsWithCommits.length} session(s)`);
|
|
4547
4928
|
const isReadyToSync = sessionsWithCommits.length > 0;
|
|
4548
4929
|
if (isReadyToSync) {
|
|
4549
|
-
|
|
4930
|
+
log11.info(` ${sessionsWithCommits.length} session(s) ready to sync on next push`);
|
|
4550
4931
|
} else {
|
|
4551
|
-
|
|
4932
|
+
log11.info(" no sessions ready to sync (no commits captured yet)");
|
|
4552
4933
|
}
|
|
4553
4934
|
}
|
|
4554
4935
|
return ok(undefined);
|
|
@@ -4557,7 +4938,7 @@ function status() {
|
|
|
4557
4938
|
// package.json
|
|
4558
4939
|
var package_default = {
|
|
4559
4940
|
name: "@residue/cli",
|
|
4560
|
-
version: "0.0.
|
|
4941
|
+
version: "0.0.4",
|
|
4561
4942
|
repository: {
|
|
4562
4943
|
type: "git",
|
|
4563
4944
|
url: "https://github.com/butttons/residue",
|
|
@@ -4603,5 +4984,7 @@ session.command("end").description("Mark an agent session as ended").requiredOpt
|
|
|
4603
4984
|
program2.command("capture").description("Tag pending sessions with current commit SHA (called by post-commit hook)").action(wrapHookCommand(() => capture()));
|
|
4604
4985
|
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 })));
|
|
4605
4986
|
program2.command("push").description("Upload pending sessions to worker (manual trigger)").action(wrapCommand(() => push()));
|
|
4987
|
+
program2.command("clear").description("Remove pending sessions from the local queue").option("--id <session-id>", "Clear a specific session by ID").action(wrapCommand((opts) => clear({ id: opts.id })));
|
|
4606
4988
|
program2.command("status").description("Show current residue state for this project").action(wrapCommand(() => status()));
|
|
4989
|
+
program2.command("search").description("Search session history").argument("<query>", "Search query").option("--ai", "Use AI-powered search (generates an answer with citations)").action(wrapCommand((query, opts) => search({ query, isAi: opts.ai })));
|
|
4607
4990
|
program2.parse();
|