terminalhire 0.27.0 → 0.28.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.
- package/dist/bin/jpi-claim.js +84 -3
- package/dist/bin/jpi-dispatch.js +85 -4
- package/package.json +1 -1
package/dist/bin/jpi-claim.js
CHANGED
|
@@ -8882,6 +8882,10 @@ ${p.body ?? ""}`)) {
|
|
|
8882
8882
|
}
|
|
8883
8883
|
return matched;
|
|
8884
8884
|
}
|
|
8885
|
+
function selectCompetingPrs(prs, selfLogin) {
|
|
8886
|
+
if (!Array.isArray(prs)) return [];
|
|
8887
|
+
return prs.filter((pr) => pr && pr.author !== selfLogin);
|
|
8888
|
+
}
|
|
8885
8889
|
async function listOpenPRsReferencingIssue(repoFullName, issueNumber) {
|
|
8886
8890
|
try {
|
|
8887
8891
|
const res = await fetch(`${GH_API2}/repos/${repoFullName}/pulls?state=open&per_page=100`, {
|
|
@@ -8947,6 +8951,67 @@ async function fetchIssue(repoFullName, issueNumber) {
|
|
|
8947
8951
|
return null;
|
|
8948
8952
|
}
|
|
8949
8953
|
}
|
|
8954
|
+
var ASSIGNMENT_MARKER = "<!-- terminalhire:assignment-request -->";
|
|
8955
|
+
var TAKE_BOT_REPOS = /* @__PURE__ */ new Set(["paradedb/paradedb"]);
|
|
8956
|
+
function buildAssignmentComment(repoFullName) {
|
|
8957
|
+
const usesTakeBot = TAKE_BOT_REPOS.has(repoFullName.toLowerCase());
|
|
8958
|
+
const body = usesTakeBot ? "/take" : `I'd like to work on this \u2014 could I be assigned? Thanks!
|
|
8959
|
+
|
|
8960
|
+
${ASSIGNMENT_MARKER}`;
|
|
8961
|
+
return { usesTakeBot, body };
|
|
8962
|
+
}
|
|
8963
|
+
async function hasPriorAssignmentRequest(repoFullName, issueNumber, login) {
|
|
8964
|
+
try {
|
|
8965
|
+
const res = await fetch(
|
|
8966
|
+
`${GH_API2}/repos/${repoFullName}/issues/${issueNumber}/comments?per_page=100&sort=created&direction=desc`,
|
|
8967
|
+
{ headers: GH_HEADERS2, signal: AbortSignal.timeout(1e4) }
|
|
8968
|
+
);
|
|
8969
|
+
if (!res.ok) return false;
|
|
8970
|
+
const comments = await res.json();
|
|
8971
|
+
if (!Array.isArray(comments)) return false;
|
|
8972
|
+
return comments.some(
|
|
8973
|
+
(c) => c && c.user && c.user.login === login && typeof c.body === "string" && c.body.includes(ASSIGNMENT_MARKER)
|
|
8974
|
+
);
|
|
8975
|
+
} catch {
|
|
8976
|
+
return false;
|
|
8977
|
+
}
|
|
8978
|
+
}
|
|
8979
|
+
async function requestIssueAssignment(claim, flags = {}, ghUser) {
|
|
8980
|
+
if (flags["no-assign"]) {
|
|
8981
|
+
console.log(" (--no-assign \u2014 skipping the assignment request; request it manually before working)");
|
|
8982
|
+
return;
|
|
8983
|
+
}
|
|
8984
|
+
const parsed = parseGitHubUrl(claim.issueUrl);
|
|
8985
|
+
if (!parsed) return;
|
|
8986
|
+
const { repoFullName, number } = parsed;
|
|
8987
|
+
let login = ghUser;
|
|
8988
|
+
if (!login) {
|
|
8989
|
+
try {
|
|
8990
|
+
login = await sh("gh", ["api", "user", "-q", ".login"]);
|
|
8991
|
+
} catch {
|
|
8992
|
+
console.log(" (assignment not requested: 'gh' not authenticated \u2014 comment on the issue manually before working)");
|
|
8993
|
+
return;
|
|
8994
|
+
}
|
|
8995
|
+
}
|
|
8996
|
+
const issue = await fetchIssue(repoFullName, number);
|
|
8997
|
+
if (issue && issue.assignees.includes(login)) {
|
|
8998
|
+
console.log(` \u2713 Already assigned to @${login} on ${repoFullName}#${number}.`);
|
|
8999
|
+
return;
|
|
9000
|
+
}
|
|
9001
|
+
const { usesTakeBot, body } = buildAssignmentComment(repoFullName);
|
|
9002
|
+
if (!usesTakeBot && await hasPriorAssignmentRequest(repoFullName, number, login)) {
|
|
9003
|
+
console.log(` \u2713 Assignment already requested on ${repoFullName}#${number}.`);
|
|
9004
|
+
return;
|
|
9005
|
+
}
|
|
9006
|
+
try {
|
|
9007
|
+
await sh("gh", ["issue", "comment", String(number), "--repo", repoFullName, "--body", body]);
|
|
9008
|
+
console.log(
|
|
9009
|
+
usesTakeBot ? ` \u2713 Requested assignment via /take on ${repoFullName}#${number}.` : ` \u2713 Requested assignment on ${repoFullName}#${number}.`
|
|
9010
|
+
);
|
|
9011
|
+
} catch (err) {
|
|
9012
|
+
console.log(` (could not post the assignment request: ${err.stderr || err.message || err} \u2014 do it manually before working)`);
|
|
9013
|
+
}
|
|
9014
|
+
}
|
|
8950
9015
|
async function pollPR(prUrl) {
|
|
8951
9016
|
const p = parseGitHubUrl(prUrl);
|
|
8952
9017
|
if (!p) return null;
|
|
@@ -9436,7 +9501,7 @@ When it's done: terminalhire claim submit ${id}`);
|
|
|
9436
9501
|
}
|
|
9437
9502
|
}
|
|
9438
9503
|
if (flags.here) {
|
|
9439
|
-
await cmdStartHere(claims, claim);
|
|
9504
|
+
await cmdStartHere(claims, claim, flags);
|
|
9440
9505
|
return;
|
|
9441
9506
|
}
|
|
9442
9507
|
let ghUser;
|
|
@@ -9498,6 +9563,7 @@ terminalhire claim: not started \u2014 starting forks ${claim.repoFullName} to y
|
|
|
9498
9563
|
await sh("git", ["-C", destDir, "checkout", "-b", branch]);
|
|
9499
9564
|
const toplevel = await sh("git", ["-C", destDir, "rev-parse", "--show-toplevel"]);
|
|
9500
9565
|
claims.updateClaim(id, { worktreePath: toplevel, branch, state: "working" });
|
|
9566
|
+
await requestIssueAssignment(claim, flags, ghUser);
|
|
9501
9567
|
console.log(`
|
|
9502
9568
|
\u2713 Started: ${claim.title}`);
|
|
9503
9569
|
console.log(` fork: ${forkFullName}`);
|
|
@@ -9507,7 +9573,7 @@ terminalhire claim: not started \u2014 starting forks ${claim.repoFullName} to y
|
|
|
9507
9573
|
console.log("\n When the work is done (the only step that pushes + opens the PR):");
|
|
9508
9574
|
console.log(` terminalhire claim submit ${id}`);
|
|
9509
9575
|
}
|
|
9510
|
-
async function cmdStartHere(claims, claim) {
|
|
9576
|
+
async function cmdStartHere(claims, claim, flags = {}) {
|
|
9511
9577
|
let toplevel;
|
|
9512
9578
|
try {
|
|
9513
9579
|
toplevel = await sh("git", ["-C", process.cwd(), "rev-parse", "--show-toplevel"]);
|
|
@@ -9537,6 +9603,7 @@ async function cmdStartHere(claims, claim) {
|
|
|
9537
9603
|
const branch = startBranchFor(claim.repoFullName, issueNumber);
|
|
9538
9604
|
await sh("git", ["-C", toplevel, "checkout", "-b", branch]);
|
|
9539
9605
|
claims.updateClaim(claim.id, { worktreePath: toplevel, branch, state: "working" });
|
|
9606
|
+
await requestIssueAssignment(claim, flags);
|
|
9540
9607
|
console.log(`
|
|
9541
9608
|
\u2713 Started here: ${claim.title}`);
|
|
9542
9609
|
console.log(` worktree: ${toplevel}`);
|
|
@@ -9768,10 +9835,22 @@ async function cmdSubmit(id, flags = {}) {
|
|
|
9768
9835
|
const issueNo = (parseGitHubUrl(claim.issueUrl) || {}).number;
|
|
9769
9836
|
const contention = issueNo ? await listOpenPRsReferencingIssue(claim.repoFullName, issueNo) : null;
|
|
9770
9837
|
if (contention && contention.total > 0) {
|
|
9771
|
-
console.log(` \u26A0 contention: ${contention.total} open PR(s) already reference this issue:`);
|
|
9772
9838
|
const nowMs = Date.now();
|
|
9839
|
+
console.log(` \u26A0 contention: ${contention.total} open PR(s) already reference this issue:`);
|
|
9773
9840
|
for (const pr of contention.prs) console.log(fmtContentionPr(pr, nowMs, false));
|
|
9774
9841
|
console.log(CONTENTION_HINT);
|
|
9842
|
+
const competing = selectCompetingPrs(contention.prs, ghUser);
|
|
9843
|
+
if (competing.length > 0 && !flags["force-competing"]) {
|
|
9844
|
+
console.error(
|
|
9845
|
+
`
|
|
9846
|
+
terminalhire claim: refusing to submit \u2014 ${competing.length} open PR(s) by someone else already address ${claim.repoFullName}#${issueNo}:`
|
|
9847
|
+
);
|
|
9848
|
+
for (const pr of competing) console.error(fmtContentionPr(pr, nowMs, false));
|
|
9849
|
+
console.error(
|
|
9850
|
+
"\n Opening a competing PR reads as low-effort duplication and burns maintainer goodwill.\n Stand down, or add value on the existing PR instead (a review, a test, a comment).\n If you are certain yours should still go up, re-run with --force-competing."
|
|
9851
|
+
);
|
|
9852
|
+
process.exit(1);
|
|
9853
|
+
}
|
|
9775
9854
|
}
|
|
9776
9855
|
}
|
|
9777
9856
|
let ok;
|
|
@@ -10187,6 +10266,7 @@ export {
|
|
|
10187
10266
|
AI_DISCLOSURE_NOTE,
|
|
10188
10267
|
CLAIM_CONSENT_VERSION,
|
|
10189
10268
|
backgroundEnableFailed,
|
|
10269
|
+
buildAssignmentComment,
|
|
10190
10270
|
buildSubmitBody,
|
|
10191
10271
|
cmdRecord,
|
|
10192
10272
|
countOpenPRsReferencingIssue,
|
|
@@ -10207,6 +10287,7 @@ export {
|
|
|
10207
10287
|
resolveSubmitWorktree,
|
|
10208
10288
|
revokeFailureAction,
|
|
10209
10289
|
run,
|
|
10290
|
+
selectCompetingPrs,
|
|
10210
10291
|
selectPushRemote,
|
|
10211
10292
|
startBranchFor,
|
|
10212
10293
|
workDirFor
|
package/dist/bin/jpi-dispatch.js
CHANGED
|
@@ -11035,6 +11035,7 @@ __export(jpi_claim_exports, {
|
|
|
11035
11035
|
AI_DISCLOSURE_NOTE: () => AI_DISCLOSURE_NOTE,
|
|
11036
11036
|
CLAIM_CONSENT_VERSION: () => CLAIM_CONSENT_VERSION,
|
|
11037
11037
|
backgroundEnableFailed: () => backgroundEnableFailed,
|
|
11038
|
+
buildAssignmentComment: () => buildAssignmentComment,
|
|
11038
11039
|
buildSubmitBody: () => buildSubmitBody,
|
|
11039
11040
|
cmdRecord: () => cmdRecord,
|
|
11040
11041
|
countOpenPRsReferencingIssue: () => countOpenPRsReferencingIssue,
|
|
@@ -11055,6 +11056,7 @@ __export(jpi_claim_exports, {
|
|
|
11055
11056
|
resolveSubmitWorktree: () => resolveSubmitWorktree,
|
|
11056
11057
|
revokeFailureAction: () => revokeFailureAction,
|
|
11057
11058
|
run: () => run7,
|
|
11059
|
+
selectCompetingPrs: () => selectCompetingPrs,
|
|
11058
11060
|
selectPushRemote: () => selectPushRemote,
|
|
11059
11061
|
startBranchFor: () => startBranchFor,
|
|
11060
11062
|
workDirFor: () => workDirFor
|
|
@@ -11288,6 +11290,10 @@ ${p.body ?? ""}`)) {
|
|
|
11288
11290
|
}
|
|
11289
11291
|
return matched;
|
|
11290
11292
|
}
|
|
11293
|
+
function selectCompetingPrs(prs, selfLogin) {
|
|
11294
|
+
if (!Array.isArray(prs)) return [];
|
|
11295
|
+
return prs.filter((pr) => pr && pr.author !== selfLogin);
|
|
11296
|
+
}
|
|
11291
11297
|
async function listOpenPRsReferencingIssue(repoFullName, issueNumber) {
|
|
11292
11298
|
try {
|
|
11293
11299
|
const res = await fetch(`${GH_API2}/repos/${repoFullName}/pulls?state=open&per_page=100`, {
|
|
@@ -11353,6 +11359,65 @@ async function fetchIssue(repoFullName, issueNumber) {
|
|
|
11353
11359
|
return null;
|
|
11354
11360
|
}
|
|
11355
11361
|
}
|
|
11362
|
+
function buildAssignmentComment(repoFullName) {
|
|
11363
|
+
const usesTakeBot = TAKE_BOT_REPOS.has(repoFullName.toLowerCase());
|
|
11364
|
+
const body = usesTakeBot ? "/take" : `I'd like to work on this \u2014 could I be assigned? Thanks!
|
|
11365
|
+
|
|
11366
|
+
${ASSIGNMENT_MARKER}`;
|
|
11367
|
+
return { usesTakeBot, body };
|
|
11368
|
+
}
|
|
11369
|
+
async function hasPriorAssignmentRequest(repoFullName, issueNumber, login) {
|
|
11370
|
+
try {
|
|
11371
|
+
const res = await fetch(
|
|
11372
|
+
`${GH_API2}/repos/${repoFullName}/issues/${issueNumber}/comments?per_page=100&sort=created&direction=desc`,
|
|
11373
|
+
{ headers: GH_HEADERS2, signal: AbortSignal.timeout(1e4) }
|
|
11374
|
+
);
|
|
11375
|
+
if (!res.ok) return false;
|
|
11376
|
+
const comments = await res.json();
|
|
11377
|
+
if (!Array.isArray(comments)) return false;
|
|
11378
|
+
return comments.some(
|
|
11379
|
+
(c) => c && c.user && c.user.login === login && typeof c.body === "string" && c.body.includes(ASSIGNMENT_MARKER)
|
|
11380
|
+
);
|
|
11381
|
+
} catch {
|
|
11382
|
+
return false;
|
|
11383
|
+
}
|
|
11384
|
+
}
|
|
11385
|
+
async function requestIssueAssignment(claim, flags = {}, ghUser) {
|
|
11386
|
+
if (flags["no-assign"]) {
|
|
11387
|
+
console.log(" (--no-assign \u2014 skipping the assignment request; request it manually before working)");
|
|
11388
|
+
return;
|
|
11389
|
+
}
|
|
11390
|
+
const parsed = parseGitHubUrl(claim.issueUrl);
|
|
11391
|
+
if (!parsed) return;
|
|
11392
|
+
const { repoFullName, number: number3 } = parsed;
|
|
11393
|
+
let login = ghUser;
|
|
11394
|
+
if (!login) {
|
|
11395
|
+
try {
|
|
11396
|
+
login = await sh("gh", ["api", "user", "-q", ".login"]);
|
|
11397
|
+
} catch {
|
|
11398
|
+
console.log(" (assignment not requested: 'gh' not authenticated \u2014 comment on the issue manually before working)");
|
|
11399
|
+
return;
|
|
11400
|
+
}
|
|
11401
|
+
}
|
|
11402
|
+
const issue2 = await fetchIssue(repoFullName, number3);
|
|
11403
|
+
if (issue2 && issue2.assignees.includes(login)) {
|
|
11404
|
+
console.log(` \u2713 Already assigned to @${login} on ${repoFullName}#${number3}.`);
|
|
11405
|
+
return;
|
|
11406
|
+
}
|
|
11407
|
+
const { usesTakeBot, body } = buildAssignmentComment(repoFullName);
|
|
11408
|
+
if (!usesTakeBot && await hasPriorAssignmentRequest(repoFullName, number3, login)) {
|
|
11409
|
+
console.log(` \u2713 Assignment already requested on ${repoFullName}#${number3}.`);
|
|
11410
|
+
return;
|
|
11411
|
+
}
|
|
11412
|
+
try {
|
|
11413
|
+
await sh("gh", ["issue", "comment", String(number3), "--repo", repoFullName, "--body", body]);
|
|
11414
|
+
console.log(
|
|
11415
|
+
usesTakeBot ? ` \u2713 Requested assignment via /take on ${repoFullName}#${number3}.` : ` \u2713 Requested assignment on ${repoFullName}#${number3}.`
|
|
11416
|
+
);
|
|
11417
|
+
} catch (err) {
|
|
11418
|
+
console.log(` (could not post the assignment request: ${err.stderr || err.message || err} \u2014 do it manually before working)`);
|
|
11419
|
+
}
|
|
11420
|
+
}
|
|
11356
11421
|
async function pollPR(prUrl) {
|
|
11357
11422
|
const p = parseGitHubUrl(prUrl);
|
|
11358
11423
|
if (!p) return null;
|
|
@@ -11842,7 +11907,7 @@ When it's done: terminalhire claim submit ${id}`);
|
|
|
11842
11907
|
}
|
|
11843
11908
|
}
|
|
11844
11909
|
if (flags.here) {
|
|
11845
|
-
await cmdStartHere(claims, claim);
|
|
11910
|
+
await cmdStartHere(claims, claim, flags);
|
|
11846
11911
|
return;
|
|
11847
11912
|
}
|
|
11848
11913
|
let ghUser;
|
|
@@ -11904,6 +11969,7 @@ terminalhire claim: not started \u2014 starting forks ${claim.repoFullName} to y
|
|
|
11904
11969
|
await sh("git", ["-C", destDir, "checkout", "-b", branch]);
|
|
11905
11970
|
const toplevel = await sh("git", ["-C", destDir, "rev-parse", "--show-toplevel"]);
|
|
11906
11971
|
claims.updateClaim(id, { worktreePath: toplevel, branch, state: "working" });
|
|
11972
|
+
await requestIssueAssignment(claim, flags, ghUser);
|
|
11907
11973
|
console.log(`
|
|
11908
11974
|
\u2713 Started: ${claim.title}`);
|
|
11909
11975
|
console.log(` fork: ${forkFullName}`);
|
|
@@ -11913,7 +11979,7 @@ terminalhire claim: not started \u2014 starting forks ${claim.repoFullName} to y
|
|
|
11913
11979
|
console.log("\n When the work is done (the only step that pushes + opens the PR):");
|
|
11914
11980
|
console.log(` terminalhire claim submit ${id}`);
|
|
11915
11981
|
}
|
|
11916
|
-
async function cmdStartHere(claims, claim) {
|
|
11982
|
+
async function cmdStartHere(claims, claim, flags = {}) {
|
|
11917
11983
|
let toplevel;
|
|
11918
11984
|
try {
|
|
11919
11985
|
toplevel = await sh("git", ["-C", process.cwd(), "rev-parse", "--show-toplevel"]);
|
|
@@ -11943,6 +12009,7 @@ async function cmdStartHere(claims, claim) {
|
|
|
11943
12009
|
const branch = startBranchFor(claim.repoFullName, issueNumber);
|
|
11944
12010
|
await sh("git", ["-C", toplevel, "checkout", "-b", branch]);
|
|
11945
12011
|
claims.updateClaim(claim.id, { worktreePath: toplevel, branch, state: "working" });
|
|
12012
|
+
await requestIssueAssignment(claim, flags);
|
|
11946
12013
|
console.log(`
|
|
11947
12014
|
\u2713 Started here: ${claim.title}`);
|
|
11948
12015
|
console.log(` worktree: ${toplevel}`);
|
|
@@ -12174,10 +12241,22 @@ async function cmdSubmit(id, flags = {}) {
|
|
|
12174
12241
|
const issueNo = (parseGitHubUrl(claim.issueUrl) || {}).number;
|
|
12175
12242
|
const contention = issueNo ? await listOpenPRsReferencingIssue(claim.repoFullName, issueNo) : null;
|
|
12176
12243
|
if (contention && contention.total > 0) {
|
|
12177
|
-
console.log(` \u26A0 contention: ${contention.total} open PR(s) already reference this issue:`);
|
|
12178
12244
|
const nowMs = Date.now();
|
|
12245
|
+
console.log(` \u26A0 contention: ${contention.total} open PR(s) already reference this issue:`);
|
|
12179
12246
|
for (const pr of contention.prs) console.log(fmtContentionPr(pr, nowMs, false));
|
|
12180
12247
|
console.log(CONTENTION_HINT);
|
|
12248
|
+
const competing = selectCompetingPrs(contention.prs, ghUser);
|
|
12249
|
+
if (competing.length > 0 && !flags["force-competing"]) {
|
|
12250
|
+
console.error(
|
|
12251
|
+
`
|
|
12252
|
+
terminalhire claim: refusing to submit \u2014 ${competing.length} open PR(s) by someone else already address ${claim.repoFullName}#${issueNo}:`
|
|
12253
|
+
);
|
|
12254
|
+
for (const pr of competing) console.error(fmtContentionPr(pr, nowMs, false));
|
|
12255
|
+
console.error(
|
|
12256
|
+
"\n Opening a competing PR reads as low-effort duplication and burns maintainer goodwill.\n Stand down, or add value on the existing PR instead (a review, a test, a comment).\n If you are certain yours should still go up, re-run with --force-competing."
|
|
12257
|
+
);
|
|
12258
|
+
process.exit(1);
|
|
12259
|
+
}
|
|
12181
12260
|
}
|
|
12182
12261
|
}
|
|
12183
12262
|
let ok;
|
|
@@ -12589,7 +12668,7 @@ async function run7() {
|
|
|
12589
12668
|
process.exit(1);
|
|
12590
12669
|
}
|
|
12591
12670
|
}
|
|
12592
|
-
var TERMINALHIRE_DIR13, INDEX_CACHE_FILE5, CLAIM_PUSH_MARKER, API_URL6, CLAIM_SYNC_BASE2, CLAIM_CONSENT_VERSION, CLAIM_POLL_INTERVAL_MS, CLAIM_POLL_TIMEOUT_MS, GH_API2, GH_HEADERS2, CONTENTION_HINT, AI_DISCLOSURE_NOTE, pExecFile, VALUE_FLAGS;
|
|
12671
|
+
var TERMINALHIRE_DIR13, INDEX_CACHE_FILE5, CLAIM_PUSH_MARKER, API_URL6, CLAIM_SYNC_BASE2, CLAIM_CONSENT_VERSION, CLAIM_POLL_INTERVAL_MS, CLAIM_POLL_TIMEOUT_MS, GH_API2, GH_HEADERS2, CONTENTION_HINT, AI_DISCLOSURE_NOTE, pExecFile, VALUE_FLAGS, ASSIGNMENT_MARKER, TAKE_BOT_REPOS;
|
|
12593
12672
|
var init_jpi_claim = __esm({
|
|
12594
12673
|
"bin/jpi-claim.js"() {
|
|
12595
12674
|
"use strict";
|
|
@@ -12611,6 +12690,8 @@ var init_jpi_claim = __esm({
|
|
|
12611
12690
|
AI_DISCLOSURE_NOTE = "---\nThis contribution was developed with AI assistance via [terminalhire](https://terminalhire.com). The author has reviewed the change and takes responsibility for its content.";
|
|
12612
12691
|
pExecFile = promisify(execFile);
|
|
12613
12692
|
VALUE_FLAGS = /* @__PURE__ */ new Set(["worktree", "branch", "body-file", "title"]);
|
|
12693
|
+
ASSIGNMENT_MARKER = "<!-- terminalhire:assignment-request -->";
|
|
12694
|
+
TAKE_BOT_REPOS = /* @__PURE__ */ new Set(["paradedb/paradedb"]);
|
|
12614
12695
|
}
|
|
12615
12696
|
});
|
|
12616
12697
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "terminalhire",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.28.0",
|
|
4
4
|
"description": "Local-first job matching for developers — ambient job matches in the Claude Code spinner. Matching runs on your machine; your profile never leaves it.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|