@p4code/cli 0.2.13 → 0.2.15
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.mjs +627 -173
- package/dist/client/assets/DiffPanel-CS8LWBsp.js +98 -0
- package/dist/client/assets/FilePreviewPanel-Detgmcd-.js +2230 -0
- package/dist/client/assets/PreviewPanel-C94P3oPz.js +2 -0
- package/dist/client/assets/{PullRequestCodeTab-W8qiPRdp.js → PullRequestCodeTab-q9KPgGC-.js} +4 -4
- package/dist/client/assets/arrow-right-C_Eu_J71.js +2 -0
- package/dist/client/assets/{fileCommentAnnotations-Bz7hRBr5.js → fileCommentAnnotations-Dh8Om-Nh.js} +2 -2
- package/dist/client/assets/{index-C8Y55igL.js → index-BPZitrC3.js} +250 -236
- package/dist/client/assets/index-Dd_1sMU1.css +1 -0
- package/dist/client/assets/{previewAssetResource-aHb2X--j.js → previewAssetResource-BEwdA2I3.js} +3 -3
- package/dist/client/assets/{renderFileChildren-EPU9YMX4.js → renderFileChildren-fTTWkGkN.js} +2 -2
- package/dist/client/assets/toggle-group-BZtpL0SP.js +2 -0
- package/dist/client/index.html +3 -3
- package/package.json +2 -1
- package/dist/client/assets/DiffPanel-B_9cbAEX.js +0 -98
- package/dist/client/assets/FilePreviewPanel-CouUd6bf.js +0 -2230
- package/dist/client/assets/PreviewPanel-CQm_NEQV.js +0 -2
- package/dist/client/assets/arrow-right-B-VCk65x.js +0 -2
- package/dist/client/assets/index-R7b1hMQC.css +0 -1
- package/dist/client/assets/toggle-group-Big0MwUC.js +0 -2
package/dist/bin.mjs
CHANGED
|
@@ -99,6 +99,7 @@ import * as Logger from "effect/Logger";
|
|
|
99
99
|
import * as NodeURL from "node:url";
|
|
100
100
|
import { createOpencodeClient } from "@opencode-ai/sdk/v2";
|
|
101
101
|
import { query } from "@anthropic-ai/claude-agent-sdk";
|
|
102
|
+
import { structuredPatch } from "diff";
|
|
102
103
|
import * as Stdio from "effect/Stdio";
|
|
103
104
|
import * as Sink from "effect/Sink";
|
|
104
105
|
import "effect/Types";
|
|
@@ -237,7 +238,7 @@ const make$89 = () => {
|
|
|
237
238
|
const layer$80 = Layer.sync(NetService, make$89);
|
|
238
239
|
//#endregion
|
|
239
240
|
//#region package.json
|
|
240
|
-
var version = "0.2.
|
|
241
|
+
var version = "0.2.15";
|
|
241
242
|
//#endregion
|
|
242
243
|
//#region src/config.ts
|
|
243
244
|
/**
|
|
@@ -10868,6 +10869,7 @@ var FeedSource = class extends Schema$1.Class("FeedSource")({
|
|
|
10868
10869
|
id: FeedSourceId,
|
|
10869
10870
|
name: Schema$1.String,
|
|
10870
10871
|
kind: FeedSourceKind,
|
|
10872
|
+
category: Schema$1.optional(Schema$1.NullOr(Schema$1.String)),
|
|
10871
10873
|
feedUrl: Schema$1.String,
|
|
10872
10874
|
siteUrl: Schema$1.String,
|
|
10873
10875
|
enabled: Schema$1.Boolean,
|
|
@@ -10893,6 +10895,7 @@ var FeedArticle = class extends Schema$1.Class("FeedArticle")({
|
|
|
10893
10895
|
id: FeedArticleId,
|
|
10894
10896
|
sourceId: FeedSourceId,
|
|
10895
10897
|
sourceName: Schema$1.String,
|
|
10898
|
+
category: Schema$1.optional(Schema$1.NullOr(Schema$1.String)),
|
|
10896
10899
|
canonicalUrl: Schema$1.String,
|
|
10897
10900
|
discussionUrl: Schema$1.NullOr(Schema$1.String),
|
|
10898
10901
|
title: Schema$1.String,
|
|
@@ -10908,6 +10911,7 @@ var FeedArticle = class extends Schema$1.Class("FeedArticle")({
|
|
|
10908
10911
|
var FeedListInput = class extends Schema$1.Class("FeedListInput")({
|
|
10909
10912
|
query: Schema$1.optional(Schema$1.String),
|
|
10910
10913
|
sourceId: Schema$1.optional(FeedSourceId),
|
|
10914
|
+
category: Schema$1.optional(Schema$1.String),
|
|
10911
10915
|
unreadOnly: Schema$1.optional(Schema$1.Boolean),
|
|
10912
10916
|
startDate: Schema$1.optional(Schema$1.String),
|
|
10913
10917
|
endDate: Schema$1.optional(Schema$1.String),
|
|
@@ -10921,6 +10925,7 @@ var FeedSourceUpsertInput = class extends Schema$1.Class("FeedSourceUpsertInput"
|
|
|
10921
10925
|
id: Schema$1.optional(FeedSourceId),
|
|
10922
10926
|
name: Schema$1.String,
|
|
10923
10927
|
kind: FeedSourceKind,
|
|
10928
|
+
category: Schema$1.optional(Schema$1.NullOr(Schema$1.String)),
|
|
10924
10929
|
feedUrl: Schema$1.String,
|
|
10925
10930
|
siteUrl: Schema$1.String,
|
|
10926
10931
|
enabled: Schema$1.Boolean
|
|
@@ -10943,7 +10948,8 @@ var FeedRefreshResult = class extends Schema$1.Class("FeedRefreshResult")({
|
|
|
10943
10948
|
fetched: Schema$1.Number,
|
|
10944
10949
|
summarized: Schema$1.Number,
|
|
10945
10950
|
failedSources: Schema$1.Array(Schema$1.String),
|
|
10946
|
-
refreshedAt: Schema$1.String
|
|
10951
|
+
refreshedAt: Schema$1.String,
|
|
10952
|
+
alreadyRunning: Schema$1.optional(Schema$1.Boolean)
|
|
10947
10953
|
}) {};
|
|
10948
10954
|
var FeedError = class extends Schema$1.TaggedErrorClass()("FeedError", {
|
|
10949
10955
|
code: Schema$1.Literals([
|
|
@@ -17894,7 +17900,7 @@ const deleteAssetRoute = HttpRouter.add("DELETE", "/assets/:kind/:name", respond
|
|
|
17894
17900
|
const outcome = yield* (yield* AgentAssetRepository).deleteByName(requested);
|
|
17895
17901
|
return outcome._tag === "conflict" ? assetConflict(Option.getOrNull(outcome.current)) : HttpServerResponse.empty({ status: 204 });
|
|
17896
17902
|
})));
|
|
17897
|
-
const feedSourceColumns = `id, name, kind, feed_url AS "feedUrl", site_url AS "siteUrl",
|
|
17903
|
+
const feedSourceColumns$1 = `id, name, kind, category, feed_url AS "feedUrl", site_url AS "siteUrl",
|
|
17898
17904
|
enabled, created_at AS "createdAt", updated_at AS "updatedAt"`;
|
|
17899
17905
|
const toFeedSource = (row) => ({
|
|
17900
17906
|
...row,
|
|
@@ -17902,6 +17908,7 @@ const toFeedSource = (row) => ({
|
|
|
17902
17908
|
});
|
|
17903
17909
|
const FeedArticleWrite = Schema$1.Struct({
|
|
17904
17910
|
sourceId: Schema$1.String,
|
|
17911
|
+
category: Schema$1.optional(Schema$1.NullOr(Schema$1.String)),
|
|
17905
17912
|
canonicalUrl: Schema$1.String,
|
|
17906
17913
|
discussionUrl: Schema$1.NullOr(Schema$1.String),
|
|
17907
17914
|
title: Schema$1.String,
|
|
@@ -17927,7 +17934,7 @@ const FeedArticleWrite = Schema$1.Struct({
|
|
|
17927
17934
|
});
|
|
17928
17935
|
const listFeedSourcesRoute = HttpRouter.add("GET", "/feed/sources", respondToHubFailures(Effect.gen(function* () {
|
|
17929
17936
|
yield* authenticateHubRequest();
|
|
17930
|
-
const rows = yield* (yield* SqlClient.SqlClient).unsafe(`SELECT ${feedSourceColumns} FROM feed_sources ORDER BY name`);
|
|
17937
|
+
const rows = yield* (yield* SqlClient.SqlClient).unsafe(`SELECT ${feedSourceColumns$1} FROM feed_sources ORDER BY name`);
|
|
17931
17938
|
return HttpServerResponse.jsonUnsafe(rows.map(toFeedSource));
|
|
17932
17939
|
})));
|
|
17933
17940
|
const upsertFeedSourceRoute = HttpRouter.add("PUT", "/feed/sources", respondToHubFailures(Effect.gen(function* () {
|
|
@@ -17937,11 +17944,11 @@ const upsertFeedSourceRoute = HttpRouter.add("PUT", "/feed/sources", respondToHu
|
|
|
17937
17944
|
const crypto = yield* Crypto.Crypto;
|
|
17938
17945
|
const id = input.id ?? FeedSourceId.make(yield* crypto.randomUUIDv4.pipe(Effect.orDie));
|
|
17939
17946
|
const now = DateTime.formatIso(yield* DateTime.now);
|
|
17940
|
-
yield* sql`INSERT INTO feed_sources (id, name, kind, feed_url, site_url, enabled, created_at, updated_at)
|
|
17941
|
-
VALUES (${id}, ${input.name}, ${input.kind}, ${input.feedUrl}, ${input.siteUrl}, ${input.enabled ? 1 : 0}, ${now}, ${now})
|
|
17942
|
-
ON CONFLICT(id) DO UPDATE SET name=excluded.name, kind=excluded.kind, feed_url=excluded.feed_url,
|
|
17947
|
+
yield* sql`INSERT INTO feed_sources (id, name, kind, category, feed_url, site_url, enabled, created_at, updated_at)
|
|
17948
|
+
VALUES (${id}, ${input.name}, ${input.kind}, ${input.category ?? null}, ${input.feedUrl}, ${input.siteUrl}, ${input.enabled ? 1 : 0}, ${now}, ${now})
|
|
17949
|
+
ON CONFLICT(id) DO UPDATE SET name=excluded.name, kind=excluded.kind, category=excluded.category, feed_url=excluded.feed_url,
|
|
17943
17950
|
site_url=excluded.site_url, enabled=excluded.enabled, updated_at=excluded.updated_at`;
|
|
17944
|
-
const rows = yield* sql.unsafe(`SELECT ${feedSourceColumns} FROM feed_sources WHERE id = ?`, [id]);
|
|
17951
|
+
const rows = yield* sql.unsafe(`SELECT ${feedSourceColumns$1} FROM feed_sources WHERE id = ?`, [id]);
|
|
17945
17952
|
return HttpServerResponse.jsonUnsafe(toFeedSource(rows[0] ?? {}));
|
|
17946
17953
|
})));
|
|
17947
17954
|
const deleteFeedSourceRoute = HttpRouter.add("DELETE", "/feed/sources/:sourceId", respondToHubFailures(Effect.gen(function* () {
|
|
@@ -17954,7 +17961,7 @@ const listFeedRoute = HttpRouter.add("GET", "/feed/articles", respondToHubFailur
|
|
|
17954
17961
|
yield* authenticateHubRequest();
|
|
17955
17962
|
const sql = yield* SqlClient.SqlClient;
|
|
17956
17963
|
const articles = yield* sql`
|
|
17957
|
-
SELECT a.id, a.source_id AS "sourceId", s.name AS "sourceName", a.canonical_url AS "canonicalUrl",
|
|
17964
|
+
SELECT a.id, a.source_id AS "sourceId", s.name AS "sourceName", COALESCE(a.category, s.category) AS category, a.canonical_url AS "canonicalUrl",
|
|
17958
17965
|
a.discussion_url AS "discussionUrl", a.title, a.author, a.published_at AS "publishedAt",
|
|
17959
17966
|
a.fetched_at AS "fetchedAt", a.content_text AS "contentText", a.status,
|
|
17960
17967
|
sm.model_selection AS "modelSelection", sm.summary, sm.highlights_json AS "highlightsJson",
|
|
@@ -17971,6 +17978,7 @@ const listFeedRoute = HttpRouter.add("GET", "/feed/articles", respondToHubFailur
|
|
|
17971
17978
|
id: row.id,
|
|
17972
17979
|
sourceId: row.sourceId,
|
|
17973
17980
|
sourceName: row.sourceName,
|
|
17981
|
+
category: row.category ?? null,
|
|
17974
17982
|
canonicalUrl: row.canonicalUrl,
|
|
17975
17983
|
discussionUrl: row.discussionUrl ?? null,
|
|
17976
17984
|
title: row.title,
|
|
@@ -18005,9 +18013,9 @@ const putFeedArticleRoute = HttpRouter.add("PUT", "/feed/articles/:articleId", r
|
|
|
18005
18013
|
const body = yield* decodeOrInvalid(FeedArticleWrite, "Request body does not match feed article storage contract.")(yield* readJsonBody());
|
|
18006
18014
|
const sql = yield* SqlClient.SqlClient;
|
|
18007
18015
|
const id = params.articleId ?? "";
|
|
18008
|
-
yield* sql`INSERT INTO feed_articles (id, source_id, canonical_url, discussion_url, title, author, published_at, fetched_at, content_text, status, content_revision)
|
|
18009
|
-
VALUES (${id}, ${body.sourceId}, ${body.canonicalUrl}, ${body.discussionUrl ?? null}, ${body.title}, ${body.author ?? null}, ${body.publishedAt}, ${body.fetchedAt}, ${body.contentText ?? null}, ${body.status}, ${body.contentRevision})
|
|
18010
|
-
ON CONFLICT(canonical_url) DO UPDATE SET discussion_url=excluded.discussion_url, title=excluded.title,
|
|
18016
|
+
yield* sql`INSERT INTO feed_articles (id, source_id, category, canonical_url, discussion_url, title, author, published_at, fetched_at, content_text, status, content_revision)
|
|
18017
|
+
VALUES (${id}, ${body.sourceId}, ${body.category ?? null}, ${body.canonicalUrl}, ${body.discussionUrl ?? null}, ${body.title}, ${body.author ?? null}, ${body.publishedAt}, ${body.fetchedAt}, ${body.contentText ?? null}, ${body.status}, ${body.contentRevision})
|
|
18018
|
+
ON CONFLICT(canonical_url) DO UPDATE SET category=excluded.category, discussion_url=excluded.discussion_url, title=excluded.title,
|
|
18011
18019
|
author=excluded.author, published_at=excluded.published_at, fetched_at=excluded.fetched_at,
|
|
18012
18020
|
content_text=excluded.content_text, status=excluded.status, content_revision=excluded.content_revision`;
|
|
18013
18021
|
if (body.summary) {
|
|
@@ -18023,6 +18031,21 @@ const putFeedArticleRoute = HttpRouter.add("PUT", "/feed/articles/:articleId", r
|
|
|
18023
18031
|
ON CONFLICT(article_id) DO UPDATE SET points=excluded.points, comments=excluded.comments, band=excluded.band, observed_at=excluded.observed_at`;
|
|
18024
18032
|
return HttpServerResponse.empty({ status: 204 });
|
|
18025
18033
|
})));
|
|
18034
|
+
const FeedEngagementWrite = Schema$1.Struct({
|
|
18035
|
+
points: Schema$1.NullOr(Schema$1.Number),
|
|
18036
|
+
comments: Schema$1.NullOr(Schema$1.Number),
|
|
18037
|
+
band: Schema$1.String,
|
|
18038
|
+
observedAt: Schema$1.String
|
|
18039
|
+
});
|
|
18040
|
+
const putFeedEngagementRoute = HttpRouter.add("PUT", "/feed/articles/:articleId/engagement", respondToHubFailures(Effect.gen(function* () {
|
|
18041
|
+
yield* authenticateHubRequest();
|
|
18042
|
+
const params = yield* HttpRouter.params;
|
|
18043
|
+
const engagement = yield* decodeOrInvalid(FeedEngagementWrite, "Request body does not match feed engagement contract.")(yield* readJsonBody());
|
|
18044
|
+
yield* (yield* SqlClient.SqlClient)`INSERT INTO feed_engagement (article_id, points, comments, band, observed_at)
|
|
18045
|
+
VALUES (${params.articleId ?? ""}, ${engagement.points ?? null}, ${engagement.comments ?? null}, ${engagement.band}, ${engagement.observedAt})
|
|
18046
|
+
ON CONFLICT(article_id) DO UPDATE SET points=excluded.points, comments=excluded.comments, band=excluded.band, observed_at=excluded.observed_at`;
|
|
18047
|
+
return HttpServerResponse.empty({ status: 204 });
|
|
18048
|
+
})));
|
|
18026
18049
|
const markFeedReadRoute = HttpRouter.add("PUT", "/feed/read", respondToHubFailures(Effect.gen(function* () {
|
|
18027
18050
|
yield* authenticateHubRequest();
|
|
18028
18051
|
const input = yield* decodeOrInvalid(FeedMarkReadInput, "Request body does not match FeedMarkReadInput.")(yield* readJsonBody());
|
|
@@ -18061,6 +18084,13 @@ const acquireFeedLeaseRoute = HttpRouter.add("POST", "/feed/lease", respondToHub
|
|
|
18061
18084
|
const rows = yield* sql`SELECT owner FROM feed_refresh_lease WHERE lease_key='refresh'`;
|
|
18062
18085
|
return HttpServerResponse.jsonUnsafe({ acquired: rows[0]?.owner === body.owner });
|
|
18063
18086
|
})));
|
|
18087
|
+
const renewFeedLeaseRoute = HttpRouter.add("PATCH", "/feed/lease", respondToHubFailures(Effect.gen(function* () {
|
|
18088
|
+
yield* authenticateHubRequest();
|
|
18089
|
+
const body = yield* readJsonBody();
|
|
18090
|
+
if (typeof body.owner !== "string" || typeof body.expiresAt !== "string") return yield* new InvalidHubRequest({ detail: "owner and expiresAt are required." });
|
|
18091
|
+
yield* (yield* SqlClient.SqlClient)`UPDATE feed_refresh_lease SET expires_at=${body.expiresAt} WHERE lease_key='refresh' AND owner=${body.owner}`;
|
|
18092
|
+
return HttpServerResponse.empty({ status: 204 });
|
|
18093
|
+
})));
|
|
18064
18094
|
const releaseFeedLeaseRoute = HttpRouter.add("DELETE", "/feed/lease", respondToHubFailures(Effect.gen(function* () {
|
|
18065
18095
|
yield* authenticateHubRequest();
|
|
18066
18096
|
const body = yield* readJsonBody();
|
|
@@ -18068,7 +18098,7 @@ const releaseFeedLeaseRoute = HttpRouter.add("DELETE", "/feed/lease", respondToH
|
|
|
18068
18098
|
yield* (yield* SqlClient.SqlClient)`DELETE FROM feed_refresh_lease WHERE lease_key='refresh' AND owner=${body.owner}`;
|
|
18069
18099
|
return HttpServerResponse.empty({ status: 204 });
|
|
18070
18100
|
})));
|
|
18071
|
-
const layer$70 = Layer.mergeAll(healthRoute, listTasksRoute, getTaskRoute, createTaskRoute, putTaskRoute, updateTaskRoute, deleteTaskRoute, listAssetsRoute, getAssetRoute, putAssetRoute, deleteAssetRoute, listFeedSourcesRoute, upsertFeedSourceRoute, deleteFeedSourceRoute, listFeedRoute, putFeedArticleRoute, markFeedReadRoute, cleanupFeedRoute, setFeedRefreshedAtRoute, acquireFeedLeaseRoute, releaseFeedLeaseRoute);
|
|
18101
|
+
const layer$70 = Layer.mergeAll(healthRoute, listTasksRoute, getTaskRoute, createTaskRoute, putTaskRoute, updateTaskRoute, deleteTaskRoute, listAssetsRoute, getAssetRoute, putAssetRoute, deleteAssetRoute, listFeedSourcesRoute, upsertFeedSourceRoute, deleteFeedSourceRoute, listFeedRoute, putFeedArticleRoute, putFeedEngagementRoute, markFeedReadRoute, cleanupFeedRoute, setFeedRefreshedAtRoute, acquireFeedLeaseRoute, renewFeedLeaseRoute, releaseFeedLeaseRoute);
|
|
18072
18102
|
//#endregion
|
|
18073
18103
|
//#region src/hub/Migrations/001_Tasks.ts
|
|
18074
18104
|
/**
|
|
@@ -18390,6 +18420,42 @@ var _010_DefaultFeedSources_default = Effect.gen(function* () {
|
|
|
18390
18420
|
VALUES (${id}, ${name}, ${kind}, ${feedUrl}, ${siteUrl}, 1, ${now}, ${now})`;
|
|
18391
18421
|
});
|
|
18392
18422
|
//#endregion
|
|
18423
|
+
//#region src/hub/Migrations/011_YCombinatorFeedSource.ts
|
|
18424
|
+
var _011_YCombinatorFeedSource_default = Effect.gen(function* () {
|
|
18425
|
+
const sql = yield* SqlClient.SqlClient;
|
|
18426
|
+
const now = DateTime.formatIso(yield* DateTime.now);
|
|
18427
|
+
yield* sql`INSERT OR IGNORE INTO feed_sources
|
|
18428
|
+
(id, name, kind, feed_url, site_url, enabled, created_at, updated_at)
|
|
18429
|
+
VALUES ('y-combinator', 'Y Combinator', 'syndication',
|
|
18430
|
+
'https://www.ycombinator.com/blog/rss/', 'https://www.ycombinator.com', 1, ${now}, ${now})`;
|
|
18431
|
+
yield* sql`UPDATE feed_sources
|
|
18432
|
+
SET site_url = 'https://news.ycombinator.com/news', updated_at = ${now}
|
|
18433
|
+
WHERE id = 'hacker-news'
|
|
18434
|
+
AND site_url IN ('http://news.ycombinator.com/news', 'https://news.ycombinator.com')`;
|
|
18435
|
+
});
|
|
18436
|
+
//#endregion
|
|
18437
|
+
//#region src/hub/Migrations/012_FeedSourceCategories.ts
|
|
18438
|
+
const defaultCategories = [
|
|
18439
|
+
["hacker-news", "Tech"],
|
|
18440
|
+
["y-combinator", "Startups"],
|
|
18441
|
+
["techcrunch", "Startups"],
|
|
18442
|
+
["the-verge", "Gadgets"],
|
|
18443
|
+
["ars-technica", "Science"],
|
|
18444
|
+
["wired", "Tech"]
|
|
18445
|
+
];
|
|
18446
|
+
var _012_FeedSourceCategories_default = Effect.gen(function* () {
|
|
18447
|
+
const sql = yield* SqlClient.SqlClient;
|
|
18448
|
+
const now = DateTime.formatIso(yield* DateTime.now);
|
|
18449
|
+
yield* sql`ALTER TABLE feed_sources ADD COLUMN category TEXT`;
|
|
18450
|
+
for (const [id, category] of defaultCategories) yield* sql`UPDATE feed_sources SET category = ${category}, updated_at = ${now}
|
|
18451
|
+
WHERE id = ${id} AND category IS NULL`;
|
|
18452
|
+
});
|
|
18453
|
+
//#endregion
|
|
18454
|
+
//#region src/hub/Migrations/013_FeedArticleCategories.ts
|
|
18455
|
+
var _013_FeedArticleCategories_default = Effect.gen(function* () {
|
|
18456
|
+
yield* (yield* SqlClient.SqlClient)`ALTER TABLE feed_articles ADD COLUMN category TEXT`;
|
|
18457
|
+
});
|
|
18458
|
+
//#endregion
|
|
18393
18459
|
//#region src/hub/Migrations.ts
|
|
18394
18460
|
/**
|
|
18395
18461
|
* Hub migrations.
|
|
@@ -18450,6 +18516,21 @@ const hubMigrationEntries = [
|
|
|
18450
18516
|
10,
|
|
18451
18517
|
"DefaultFeedSources",
|
|
18452
18518
|
_010_DefaultFeedSources_default
|
|
18519
|
+
],
|
|
18520
|
+
[
|
|
18521
|
+
11,
|
|
18522
|
+
"YCombinatorFeedSource",
|
|
18523
|
+
_011_YCombinatorFeedSource_default
|
|
18524
|
+
],
|
|
18525
|
+
[
|
|
18526
|
+
12,
|
|
18527
|
+
"FeedSourceCategories",
|
|
18528
|
+
_012_FeedSourceCategories_default
|
|
18529
|
+
],
|
|
18530
|
+
[
|
|
18531
|
+
13,
|
|
18532
|
+
"FeedArticleCategories",
|
|
18533
|
+
_013_FeedArticleCategories_default
|
|
18453
18534
|
]
|
|
18454
18535
|
];
|
|
18455
18536
|
const hubMigrationLoader = Migrator.fromRecord(Object.fromEntries(hubMigrationEntries.map(([id, name, migration]) => [`${id}_${name}`, migration])));
|
|
@@ -39534,6 +39615,7 @@ function projectActivityPayload(activity) {
|
|
|
39534
39615
|
const changedFiles = [];
|
|
39535
39616
|
collectChangedFiles(data, changedFiles, /* @__PURE__ */ new Set(), 0);
|
|
39536
39617
|
if (changedFiles.length > 0) projectedData.files = changedFiles.map((path) => ({ path }));
|
|
39618
|
+
if (asTrimmedString$1(data.patch)) projectedData.patch = data.patch;
|
|
39537
39619
|
const toolCategory = classifyToolCategoryFromToolData(data);
|
|
39538
39620
|
if (toolCategory) projectedData.toolCategory = toolCategory;
|
|
39539
39621
|
if ("toolCallId" in data) projectedData.toolCallId = data.toolCallId;
|
|
@@ -43527,6 +43609,133 @@ const make$50 = Effect.gen(function* () {
|
|
|
43527
43609
|
});
|
|
43528
43610
|
const layer$40 = Layer.effect(UsageService, make$50);
|
|
43529
43611
|
//#endregion
|
|
43612
|
+
//#region src/feed/FeedStore.ts
|
|
43613
|
+
const storageFailure = (message) => new FeedError({
|
|
43614
|
+
code: "hub_unavailable",
|
|
43615
|
+
message
|
|
43616
|
+
});
|
|
43617
|
+
const feedSourceColumns = `id, name, kind, category, feed_url AS "feedUrl", site_url AS "siteUrl",
|
|
43618
|
+
enabled, created_at AS "createdAt", updated_at AS "updatedAt"`;
|
|
43619
|
+
const toStoredSource = (row) => ({
|
|
43620
|
+
...row,
|
|
43621
|
+
enabled: Boolean(row.enabled)
|
|
43622
|
+
});
|
|
43623
|
+
/**
|
|
43624
|
+
* Local feed storage over the same SQLite schema the Hub uses, so a later Hub
|
|
43625
|
+
* connection can adopt the data without translation.
|
|
43626
|
+
*/
|
|
43627
|
+
const makeLocalFeedStore = Effect.gen(function* () {
|
|
43628
|
+
const sql = yield* SqlClient.SqlClient;
|
|
43629
|
+
const run = (label, effect) => effect.pipe(Effect.mapError(() => storageFailure(`Local feed storage failed (${label}).`)));
|
|
43630
|
+
const listSources = () => run("sources", sql.unsafe(`SELECT ${feedSourceColumns} FROM feed_sources ORDER BY name`).pipe(Effect.map((rows) => rows.map(toStoredSource))));
|
|
43631
|
+
return {
|
|
43632
|
+
listSources,
|
|
43633
|
+
upsertSource: (input) => run("source upsert", Effect.gen(function* () {
|
|
43634
|
+
const id = input.id ?? FeedSourceId.make(crypto.randomUUID());
|
|
43635
|
+
const now = DateTime.formatIso(yield* DateTime.now);
|
|
43636
|
+
yield* sql`INSERT INTO feed_sources (id, name, kind, category, feed_url, site_url, enabled, created_at, updated_at)
|
|
43637
|
+
VALUES (${id}, ${input.name}, ${input.kind}, ${input.category ?? null}, ${input.feedUrl}, ${input.siteUrl}, ${input.enabled ? 1 : 0}, ${now}, ${now})
|
|
43638
|
+
ON CONFLICT(id) DO UPDATE SET name=excluded.name, kind=excluded.kind, category=excluded.category, feed_url=excluded.feed_url,
|
|
43639
|
+
site_url=excluded.site_url, enabled=excluded.enabled, updated_at=excluded.updated_at`;
|
|
43640
|
+
const rows = yield* sql.unsafe(`SELECT ${feedSourceColumns} FROM feed_sources WHERE id = ?`, [id]);
|
|
43641
|
+
return toStoredSource(rows[0] ?? {});
|
|
43642
|
+
})),
|
|
43643
|
+
deleteSource: (sourceId) => run("source delete", sql`DELETE FROM feed_sources WHERE id = ${sourceId}`),
|
|
43644
|
+
listArticles: () => run("articles", Effect.gen(function* () {
|
|
43645
|
+
const rows = yield* sql`
|
|
43646
|
+
SELECT a.id, a.source_id AS "sourceId", s.name AS "sourceName", COALESCE(a.category, s.category) AS category, a.canonical_url AS "canonicalUrl",
|
|
43647
|
+
a.discussion_url AS "discussionUrl", a.title, a.author, a.published_at AS "publishedAt",
|
|
43648
|
+
a.fetched_at AS "fetchedAt", a.content_text AS "contentText", a.status,
|
|
43649
|
+
sm.model_selection AS "modelSelection", sm.summary, sm.highlights_json AS "highlightsJson",
|
|
43650
|
+
sm.references_json AS "referencesJson", sm.summarized_at AS "summarizedAt",
|
|
43651
|
+
e.points, e.comments, COALESCE(e.band, 'unknown') AS band, COALESCE(e.observed_at, a.fetched_at) AS "observedAt",
|
|
43652
|
+
r.read_at AS "readAt"
|
|
43653
|
+
FROM feed_articles a JOIN feed_sources s ON s.id=a.source_id
|
|
43654
|
+
LEFT JOIN feed_summaries sm ON sm.article_id=a.id
|
|
43655
|
+
LEFT JOIN feed_engagement e ON e.article_id=a.id
|
|
43656
|
+
LEFT JOIN feed_read_state r ON r.article_id=a.id ORDER BY a.published_at DESC`;
|
|
43657
|
+
const metadata = yield* sql`SELECT value FROM feed_metadata WHERE key='refreshed_at'`;
|
|
43658
|
+
return {
|
|
43659
|
+
articles: rows.map((row) => ({
|
|
43660
|
+
id: row.id,
|
|
43661
|
+
sourceId: row.sourceId,
|
|
43662
|
+
sourceName: row.sourceName,
|
|
43663
|
+
category: row.category ?? null,
|
|
43664
|
+
canonicalUrl: row.canonicalUrl,
|
|
43665
|
+
discussionUrl: row.discussionUrl ?? null,
|
|
43666
|
+
title: row.title,
|
|
43667
|
+
author: row.author ?? null,
|
|
43668
|
+
publishedAt: row.publishedAt,
|
|
43669
|
+
fetchedAt: row.fetchedAt,
|
|
43670
|
+
contentText: row.contentText ?? null,
|
|
43671
|
+
status: row.status,
|
|
43672
|
+
readAt: row.readAt ?? null,
|
|
43673
|
+
summary: row.summary == null ? null : {
|
|
43674
|
+
articleId: row.id,
|
|
43675
|
+
modelSelection: row.modelSelection,
|
|
43676
|
+
summary: row.summary,
|
|
43677
|
+
highlights: JSON.parse(String(row.highlightsJson)),
|
|
43678
|
+
references: JSON.parse(String(row.referencesJson)),
|
|
43679
|
+
summarizedAt: row.summarizedAt
|
|
43680
|
+
},
|
|
43681
|
+
engagement: {
|
|
43682
|
+
articleId: row.id,
|
|
43683
|
+
points: row.points ?? null,
|
|
43684
|
+
comments: row.comments ?? null,
|
|
43685
|
+
band: row.band,
|
|
43686
|
+
observedAt: row.observedAt
|
|
43687
|
+
}
|
|
43688
|
+
})),
|
|
43689
|
+
refreshedAt: metadata[0]?.value ?? null
|
|
43690
|
+
};
|
|
43691
|
+
})),
|
|
43692
|
+
putArticle: (id, write) => run("article write", Effect.gen(function* () {
|
|
43693
|
+
yield* sql`INSERT INTO feed_articles (id, source_id, category, canonical_url, discussion_url, title, author, published_at, fetched_at, content_text, status, content_revision)
|
|
43694
|
+
VALUES (${id}, ${write.sourceId}, ${write.category ?? null}, ${write.canonicalUrl}, ${write.discussionUrl ?? null}, ${write.title}, ${write.author ?? null}, ${write.publishedAt}, ${write.fetchedAt}, ${write.contentText ?? null}, ${write.status}, ${write.contentRevision})
|
|
43695
|
+
ON CONFLICT(canonical_url) DO UPDATE SET category=excluded.category, discussion_url=excluded.discussion_url, title=excluded.title,
|
|
43696
|
+
author=excluded.author, published_at=excluded.published_at, fetched_at=excluded.fetched_at,
|
|
43697
|
+
content_text=excluded.content_text, status=excluded.status, content_revision=excluded.content_revision`;
|
|
43698
|
+
if (write.summary) {
|
|
43699
|
+
const summary = write.summary;
|
|
43700
|
+
yield* sql`INSERT INTO feed_summaries (article_id, model_selection, summary, highlights_json, references_json, summarized_at, content_revision)
|
|
43701
|
+
VALUES (${id}, ${summary.modelSelection}, ${summary.summary}, ${JSON.stringify(summary.highlights)}, ${JSON.stringify(summary.references)}, ${summary.summarizedAt}, ${write.contentRevision})
|
|
43702
|
+
ON CONFLICT(article_id) DO UPDATE SET model_selection=excluded.model_selection, summary=excluded.summary,
|
|
43703
|
+
highlights_json=excluded.highlights_json, references_json=excluded.references_json, summarized_at=excluded.summarized_at, content_revision=excluded.content_revision`;
|
|
43704
|
+
}
|
|
43705
|
+
const engagement = write.engagement;
|
|
43706
|
+
yield* sql`INSERT INTO feed_engagement (article_id, points, comments, band, observed_at)
|
|
43707
|
+
VALUES (${id}, ${engagement.points ?? null}, ${engagement.comments ?? null}, ${engagement.band}, ${engagement.observedAt})
|
|
43708
|
+
ON CONFLICT(article_id) DO UPDATE SET points=excluded.points, comments=excluded.comments, band=excluded.band, observed_at=excluded.observed_at`;
|
|
43709
|
+
})),
|
|
43710
|
+
putEngagement: (articleId, engagement) => run("engagement", sql`INSERT INTO feed_engagement (article_id, points, comments, band, observed_at)
|
|
43711
|
+
VALUES (${articleId}, ${engagement.points ?? null}, ${engagement.comments ?? null}, ${engagement.band}, ${engagement.observedAt})
|
|
43712
|
+
ON CONFLICT(article_id) DO UPDATE SET points=excluded.points, comments=excluded.comments, band=excluded.band, observed_at=excluded.observed_at`),
|
|
43713
|
+
markRead: (input) => run("read state", Effect.gen(function* () {
|
|
43714
|
+
const readAt = input.read ? DateTime.formatIso(yield* DateTime.now) : null;
|
|
43715
|
+
yield* sql`INSERT INTO feed_read_state (article_id, read_at) VALUES (${input.articleId}, ${readAt})
|
|
43716
|
+
ON CONFLICT(article_id) DO UPDATE SET read_at=excluded.read_at`;
|
|
43717
|
+
})),
|
|
43718
|
+
cleanup: (input) => run("cleanup", Effect.gen(function* () {
|
|
43719
|
+
const rows = yield* sql`SELECT COUNT(*) AS count FROM feed_articles WHERE published_at >= ${input.startDate} AND published_at <= ${input.endDate}`;
|
|
43720
|
+
const count = Number(rows[0]?.count ?? 0);
|
|
43721
|
+
if (input.confirm) yield* sql`DELETE FROM feed_articles WHERE published_at >= ${input.startDate} AND published_at <= ${input.endDate}`;
|
|
43722
|
+
return new FeedCleanupResult({
|
|
43723
|
+
count,
|
|
43724
|
+
deleted: input.confirm
|
|
43725
|
+
});
|
|
43726
|
+
})),
|
|
43727
|
+
setRefreshedAt: (refreshedAt) => run("refreshed-at", sql`INSERT INTO feed_metadata (key, value) VALUES ('refreshed_at', ${refreshedAt}) ON CONFLICT(key) DO UPDATE SET value=excluded.value`),
|
|
43728
|
+
acquireLease: (owner, expiresAt) => run("lease acquire", Effect.gen(function* () {
|
|
43729
|
+
const now = DateTime.formatIso(yield* DateTime.now);
|
|
43730
|
+
yield* sql`DELETE FROM feed_refresh_lease WHERE lease_key='refresh' AND expires_at <= ${now}`;
|
|
43731
|
+
yield* sql`INSERT INTO feed_refresh_lease (lease_key, owner, expires_at) VALUES ('refresh', ${owner}, ${expiresAt}) ON CONFLICT(lease_key) DO NOTHING`;
|
|
43732
|
+
return (yield* sql`SELECT owner FROM feed_refresh_lease WHERE lease_key='refresh'`)[0]?.owner === owner;
|
|
43733
|
+
})),
|
|
43734
|
+
renewLease: (owner, expiresAt) => run("lease renew", sql`UPDATE feed_refresh_lease SET expires_at=${expiresAt} WHERE lease_key='refresh' AND owner=${owner}`),
|
|
43735
|
+
releaseLease: (owner) => run("lease release", sql`DELETE FROM feed_refresh_lease WHERE lease_key='refresh' AND owner=${owner}`)
|
|
43736
|
+
};
|
|
43737
|
+
});
|
|
43738
|
+
//#endregion
|
|
43530
43739
|
//#region src/feed/ArticleText.ts
|
|
43531
43740
|
const BLOCK_TAGS = /<\/?(?:p|div|article|section|main|h[1-6]|li|br)[^>]*>/gi;
|
|
43532
43741
|
function extractReadableArticleText(html) {
|
|
@@ -43549,6 +43758,40 @@ function hackerNewsItemId(discussionUrl) {
|
|
|
43549
43758
|
return null;
|
|
43550
43759
|
}
|
|
43551
43760
|
}
|
|
43761
|
+
const HN_ITEM_FETCH_BATCH = 20;
|
|
43762
|
+
const HN_API_BASE = "https://hacker-news.firebaseio.com/v0";
|
|
43763
|
+
async function fetchHackerNewsFrontPage(storyCount = 300) {
|
|
43764
|
+
const idsResponse = await fetch(`${HN_API_BASE}/topstories.json`);
|
|
43765
|
+
if (!idsResponse.ok) throw new Error(String(idsResponse.status));
|
|
43766
|
+
const ids = (await idsResponse.json()).slice(0, storyCount);
|
|
43767
|
+
const entries = [];
|
|
43768
|
+
for (let index = 0; index < ids.length; index += HN_ITEM_FETCH_BATCH) {
|
|
43769
|
+
const batch = ids.slice(index, index + HN_ITEM_FETCH_BATCH);
|
|
43770
|
+
const items = await Promise.all(batch.map(async (id) => {
|
|
43771
|
+
try {
|
|
43772
|
+
const response = await fetch(`${HN_API_BASE}/item/${id}.json`);
|
|
43773
|
+
return response.ok ? await response.json() : null;
|
|
43774
|
+
} catch {
|
|
43775
|
+
return null;
|
|
43776
|
+
}
|
|
43777
|
+
}));
|
|
43778
|
+
for (const item of items) {
|
|
43779
|
+
if (!item || item.type !== "story" || item.dead || item.deleted || !item.title) continue;
|
|
43780
|
+
const discussionUrl = hackerNewsDiscussionUrl(String(item.id));
|
|
43781
|
+
entries.push({
|
|
43782
|
+
title: item.title,
|
|
43783
|
+
url: item.url ?? discussionUrl,
|
|
43784
|
+
author: item.by ?? null,
|
|
43785
|
+
publishedAt: (/* @__PURE__ */ new Date((item.time ?? 0) * 1e3)).toISOString(),
|
|
43786
|
+
content: null,
|
|
43787
|
+
discussionUrl,
|
|
43788
|
+
points: item.score ?? null,
|
|
43789
|
+
comments: item.descendants ?? null
|
|
43790
|
+
});
|
|
43791
|
+
}
|
|
43792
|
+
}
|
|
43793
|
+
return entries;
|
|
43794
|
+
}
|
|
43552
43795
|
//#endregion
|
|
43553
43796
|
//#region src/feed/SyndicationSource.ts
|
|
43554
43797
|
const decodeEntities = (value) => value.replaceAll("<![CDATA[", "").replaceAll("]]>", "").replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll(""", "\"").replaceAll("'", "'").trim();
|
|
@@ -43611,10 +43854,103 @@ function parseSyndicationFeed(xml) {
|
|
|
43611
43854
|
//#endregion
|
|
43612
43855
|
//#region src/feed/FeedService.ts
|
|
43613
43856
|
const FEED_REQUEST_TIMEOUT_MS = 2e4;
|
|
43614
|
-
const FEED_REFRESH_LEASE_MS =
|
|
43857
|
+
const FEED_REFRESH_LEASE_MS = 10 * 6e4;
|
|
43858
|
+
const MORNING_REFRESH_HOUR_DEFAULT = 7;
|
|
43859
|
+
/**
|
|
43860
|
+
* Daily refresh hour (0-23, server-local time), overridable with
|
|
43861
|
+
* P4CODE_FEED_REFRESH_HOUR. Set it to "off" to disable the daily refresh.
|
|
43862
|
+
* On-demand refreshes stay available from the Feed UI regardless.
|
|
43863
|
+
*/
|
|
43864
|
+
const morningRefreshHour = (() => {
|
|
43865
|
+
const raw = process.env.P4CODE_FEED_REFRESH_HOUR;
|
|
43866
|
+
if (raw === "off") return null;
|
|
43867
|
+
const parsed = Number(raw ?? MORNING_REFRESH_HOUR_DEFAULT);
|
|
43868
|
+
return Number.isInteger(parsed) && parsed >= 0 && parsed <= 23 ? parsed : MORNING_REFRESH_HOUR_DEFAULT;
|
|
43869
|
+
})();
|
|
43615
43870
|
const FEED_FETCH_CONCURRENCY = 4;
|
|
43616
43871
|
const FEED_MODEL = "gpt-5.6-luna";
|
|
43617
43872
|
const FEED_FALLBACK_MODEL = "claude-sonnet-5";
|
|
43873
|
+
const sourceKind = (source) => source.kind ?? (source.feedUrl.includes("hnrss.org") || source.siteUrl.includes("news.ycombinator.com") ? "hacker_news" : "syndication");
|
|
43874
|
+
const normalizeFeedSource = (source) => new FeedSource({
|
|
43875
|
+
...source,
|
|
43876
|
+
kind: sourceKind(source),
|
|
43877
|
+
category: source.category ?? null
|
|
43878
|
+
});
|
|
43879
|
+
const normalizeFeedArticle = (article) => new FeedArticle({
|
|
43880
|
+
...article,
|
|
43881
|
+
discussionUrl: article.discussionUrl ?? null,
|
|
43882
|
+
author: article.author ?? null,
|
|
43883
|
+
category: article.category ?? null,
|
|
43884
|
+
summary: article.summary === null ? null : new FeedSummary({
|
|
43885
|
+
...article.summary,
|
|
43886
|
+
...normalizeStoredSummary(article.summary)
|
|
43887
|
+
}),
|
|
43888
|
+
engagement: new FeedEngagement({ ...article.engagement })
|
|
43889
|
+
});
|
|
43890
|
+
const normalizeFeedList = (result) => new FeedListResult({
|
|
43891
|
+
...result,
|
|
43892
|
+
articles: result.articles.map(normalizeFeedArticle)
|
|
43893
|
+
});
|
|
43894
|
+
/**
|
|
43895
|
+
* Aggregate sources like the Hacker News front page mix many topics, so each
|
|
43896
|
+
* article gets its own category from the summarizer. The closed list keeps
|
|
43897
|
+
* the category filter stable; unknown model output falls back to null and the
|
|
43898
|
+
* source category takes over at read time.
|
|
43899
|
+
*/
|
|
43900
|
+
const FEED_CATEGORIES = [
|
|
43901
|
+
"Tech",
|
|
43902
|
+
"AI",
|
|
43903
|
+
"Science",
|
|
43904
|
+
"Startups",
|
|
43905
|
+
"Gadgets",
|
|
43906
|
+
"Business",
|
|
43907
|
+
"Security",
|
|
43908
|
+
"Culture",
|
|
43909
|
+
"Other"
|
|
43910
|
+
];
|
|
43911
|
+
const CATEGORY_LINE_PATTERN = /^\s*(?:#+\s*)?Category:\s*([A-Za-z ]+?)\s*$/im;
|
|
43912
|
+
const extractGeneratedCategory = (body) => {
|
|
43913
|
+
const match = body.match(CATEGORY_LINE_PATTERN);
|
|
43914
|
+
if (!match) return {
|
|
43915
|
+
category: null,
|
|
43916
|
+
body
|
|
43917
|
+
};
|
|
43918
|
+
return {
|
|
43919
|
+
category: FEED_CATEGORIES.find((candidate) => candidate.toLocaleLowerCase() === match[1]?.trim().toLocaleLowerCase()) ?? null,
|
|
43920
|
+
body: body.replace(CATEGORY_LINE_PATTERN, " ")
|
|
43921
|
+
};
|
|
43922
|
+
};
|
|
43923
|
+
const normalizeGeneratedSummary = (body) => {
|
|
43924
|
+
const highlights = (body.split(/(?:^|\s)##\s+Testing\b/i, 1)[0] ?? body).replace(/^\s*##\s+Summary\b\s*/i, "").split(/\n+|\s+[-*]\s+/).map((line) => line.trim().replace(/^[-*]\s+/, "")).filter(Boolean).slice(0, 3);
|
|
43925
|
+
return {
|
|
43926
|
+
summary: highlights.join(" "),
|
|
43927
|
+
highlights
|
|
43928
|
+
};
|
|
43929
|
+
};
|
|
43930
|
+
const normalizeStoredSummary = (summary) => {
|
|
43931
|
+
const normalized = normalizeGeneratedSummary(summary.summary);
|
|
43932
|
+
const highlights = summary.highlights.flatMap((highlight) => normalizeGeneratedSummary(highlight).highlights).filter(Boolean).slice(0, 3);
|
|
43933
|
+
return {
|
|
43934
|
+
summary: normalized.summary,
|
|
43935
|
+
highlights: highlights.length > 0 ? highlights : normalized.highlights
|
|
43936
|
+
};
|
|
43937
|
+
};
|
|
43938
|
+
/**
|
|
43939
|
+
* Promotion round-ups (promo codes, coupon lists, discount posts) are noise in
|
|
43940
|
+
* a news feed. Matched articles are skipped at fetch time and hidden from
|
|
43941
|
+
* reads, so already-stored ones disappear too. Patterns are deliberately
|
|
43942
|
+
* narrow: "strikes deal" or "sale of a company" must not match.
|
|
43943
|
+
*/
|
|
43944
|
+
const PROMOTIONAL_TITLE_PATTERNS = [
|
|
43945
|
+
/promo codes?/i,
|
|
43946
|
+
/coupons?/i,
|
|
43947
|
+
/discount codes?/i,
|
|
43948
|
+
/\d+% off/i,
|
|
43949
|
+
/\$\d+ off/i,
|
|
43950
|
+
/half off/i,
|
|
43951
|
+
/save up to/i
|
|
43952
|
+
];
|
|
43953
|
+
const isPromotionalArticle = (title) => PROMOTIONAL_TITLE_PATTERNS.some((pattern) => pattern.test(title));
|
|
43618
43954
|
const fail = (code, message) => new FeedError({
|
|
43619
43955
|
code,
|
|
43620
43956
|
message
|
|
@@ -43643,171 +43979,229 @@ const jsonRequest = Effect.fn("FeedService.jsonRequest")(function* (url, token,
|
|
|
43643
43979
|
const make$49 = Effect.gen(function* () {
|
|
43644
43980
|
const hubLink = yield* HubLink;
|
|
43645
43981
|
const providers = yield* ProviderInstanceRegistry;
|
|
43646
|
-
const
|
|
43647
|
-
|
|
43648
|
-
|
|
43649
|
-
return
|
|
43650
|
-
});
|
|
43651
|
-
const call = Effect.fn("FeedService.call")(function* (path, init) {
|
|
43652
|
-
const linked = yield* settings();
|
|
43653
|
-
return yield* jsonRequest(`${linked.baseUrl}${path}`, linked.token, init);
|
|
43982
|
+
const config = yield* ServerConfig$1;
|
|
43983
|
+
const path = yield* Path.Path;
|
|
43984
|
+
const call = Effect.fn("FeedService.call")(function* (linked, requestPath, init) {
|
|
43985
|
+
return yield* jsonRequest(`${linked.baseUrl}${requestPath}`, linked.token, init);
|
|
43654
43986
|
});
|
|
43655
|
-
const
|
|
43656
|
-
|
|
43657
|
-
|
|
43658
|
-
const result = yield* call("/feed/articles");
|
|
43659
|
-
const query = input.query?.trim().toLocaleLowerCase();
|
|
43660
|
-
const articles = result.articles.filter((article) => input.sourceId === void 0 || article.sourceId === input.sourceId).filter((article) => !input.unreadOnly || article.readAt === null).filter((article) => input.startDate === void 0 || article.publishedAt >= input.startDate).filter((article) => input.endDate === void 0 || article.publishedAt <= input.endDate).filter((article) => !query || `${article.title}\n${article.summary?.summary ?? ""}\n${article.summary?.highlights.join(" ") ?? ""}`.toLocaleLowerCase().includes(query)).toSorted((a, b) => input.sort === "oldest" ? a.publishedAt.localeCompare(b.publishedAt) : b.publishedAt.localeCompare(a.publishedAt));
|
|
43661
|
-
return {
|
|
43662
|
-
...result,
|
|
43663
|
-
articles
|
|
43664
|
-
};
|
|
43665
|
-
}),
|
|
43666
|
-
refresh: Effect.fn("FeedService.refresh")(function* () {
|
|
43667
|
-
const owner = crypto.randomUUID();
|
|
43668
|
-
const refreshedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
43669
|
-
if (!(yield* call("/feed/lease", {
|
|
43670
|
-
method: "POST",
|
|
43671
|
-
body: JSON.stringify({
|
|
43672
|
-
owner,
|
|
43673
|
-
expiresAt: new Date(Date.now() + FEED_REFRESH_LEASE_MS).toISOString()
|
|
43674
|
-
})
|
|
43675
|
-
})).acquired) return {
|
|
43676
|
-
fetched: 0,
|
|
43677
|
-
summarized: 0,
|
|
43678
|
-
failedSources: [],
|
|
43679
|
-
refreshedAt
|
|
43680
|
-
};
|
|
43681
|
-
const existing = yield* call("/feed/articles");
|
|
43682
|
-
const existingByUrl = new Map(existing.articles.map((article) => [article.canonicalUrl, article]));
|
|
43683
|
-
const enabled = (yield* sources()).filter((source) => source.enabled);
|
|
43684
|
-
let fetched = 0;
|
|
43685
|
-
let summarized = 0;
|
|
43686
|
-
const failedSources = [];
|
|
43687
|
-
yield* Effect.forEach(enabled, (source) => Effect.gen(function* () {
|
|
43688
|
-
const entries = parseSyndicationFeed(yield* Effect.tryPromise({
|
|
43689
|
-
try: () => fetch(source.feedUrl).then((response) => {
|
|
43690
|
-
if (!response.ok) throw new Error(String(response.status));
|
|
43691
|
-
return response.text();
|
|
43692
|
-
}),
|
|
43693
|
-
catch: () => fail("fetch_failed", `Could not fetch ${source.name}.`)
|
|
43694
|
-
}));
|
|
43695
|
-
for (const entry of entries) {
|
|
43696
|
-
const current = existingByUrl.get(entry.url);
|
|
43697
|
-
let contentText = entry.content ? extractReadableArticleText(entry.content) : null;
|
|
43698
|
-
if (contentText === null) contentText = yield* Effect.tryPromise({
|
|
43699
|
-
try: () => fetch(entry.url).then((response) => response.ok ? response.text() : "").then(extractReadableArticleText),
|
|
43700
|
-
catch: () => null
|
|
43701
|
-
});
|
|
43702
|
-
const revision = contentText ?? `${entry.title}:${entry.publishedAt}`;
|
|
43703
|
-
if (current?.contentText === contentText && current.summary !== null) continue;
|
|
43704
|
-
const id = current?.id ?? FeedArticleId.make(crypto.randomUUID());
|
|
43705
|
-
const hnId = source.kind === "hacker_news" ? hackerNewsItemId(entry.discussionUrl) : null;
|
|
43706
|
-
const hnEngagement = hnId === null ? null : yield* Effect.tryPromise({
|
|
43707
|
-
try: () => fetch(`https://hacker-news.firebaseio.com/v0/item/${encodeURIComponent(hnId)}.json`).then((response) => response.ok ? response.json() : null),
|
|
43708
|
-
catch: () => null
|
|
43709
|
-
});
|
|
43710
|
-
let summary = null;
|
|
43711
|
-
let status = "summary_unavailable";
|
|
43712
|
-
if (contentText !== null) {
|
|
43713
|
-
const instances = yield* providers.listInstances;
|
|
43714
|
-
const candidates = [{
|
|
43715
|
-
instance: instances.find((instance) => instance.driverKind === "codex" && instance.enabled),
|
|
43716
|
-
model: FEED_MODEL
|
|
43717
|
-
}, {
|
|
43718
|
-
instance: instances.find((instance) => instance.driverKind === "claudeAgent" && instance.enabled),
|
|
43719
|
-
model: FEED_FALLBACK_MODEL
|
|
43720
|
-
}];
|
|
43721
|
-
for (const candidate of candidates) {
|
|
43722
|
-
if (!candidate.instance) continue;
|
|
43723
|
-
const generated = yield* candidate.instance.textGeneration.generatePrContent({
|
|
43724
|
-
cwd: process.cwd(),
|
|
43725
|
-
baseBranch: "feed",
|
|
43726
|
-
headBranch: "article",
|
|
43727
|
-
commitSummary: entry.title,
|
|
43728
|
-
diffSummary: "Summarize this article for a dense technology news feed. Return concise summary in body.",
|
|
43729
|
-
diffPatch: contentText.slice(0, 24e3),
|
|
43730
|
-
modelSelection: {
|
|
43731
|
-
instanceId: ProviderInstanceId.make(candidate.instance.instanceId),
|
|
43732
|
-
model: candidate.model
|
|
43733
|
-
}
|
|
43734
|
-
}).pipe(Effect.option);
|
|
43735
|
-
if (Option.isNone(generated)) continue;
|
|
43736
|
-
const body = generated.value.body.trim();
|
|
43737
|
-
summary = {
|
|
43738
|
-
modelSelection: `${candidate.instance.instanceId}:${candidate.model}`,
|
|
43739
|
-
summary: body,
|
|
43740
|
-
highlights: body.split(/(?<=[.!?])\s+/).filter(Boolean).slice(0, 3),
|
|
43741
|
-
references: [entry.url],
|
|
43742
|
-
summarizedAt: refreshedAt
|
|
43743
|
-
};
|
|
43744
|
-
status = "ready";
|
|
43745
|
-
summarized += 1;
|
|
43746
|
-
break;
|
|
43747
|
-
}
|
|
43748
|
-
if (summary === null) status = "summary_failed";
|
|
43749
|
-
}
|
|
43750
|
-
yield* call(`/feed/articles/${encodeURIComponent(id)}`, {
|
|
43751
|
-
method: "PUT",
|
|
43752
|
-
body: JSON.stringify({
|
|
43753
|
-
sourceId: source.id,
|
|
43754
|
-
canonicalUrl: entry.url,
|
|
43755
|
-
discussionUrl: hnId ? hackerNewsDiscussionUrl(hnId) : null,
|
|
43756
|
-
title: entry.title,
|
|
43757
|
-
author: entry.author,
|
|
43758
|
-
publishedAt: entry.publishedAt,
|
|
43759
|
-
fetchedAt: refreshedAt,
|
|
43760
|
-
contentText,
|
|
43761
|
-
status,
|
|
43762
|
-
contentRevision: revision,
|
|
43763
|
-
summary,
|
|
43764
|
-
engagement: hnEngagement === null ? {
|
|
43765
|
-
points: null,
|
|
43766
|
-
comments: null,
|
|
43767
|
-
band: "unknown",
|
|
43768
|
-
observedAt: refreshedAt
|
|
43769
|
-
} : {
|
|
43770
|
-
points: hnEngagement.score ?? 0,
|
|
43771
|
-
comments: hnEngagement.descendants ?? 0,
|
|
43772
|
-
band: hackerNewsEngagementBand(hnEngagement.score ?? 0, hnEngagement.descendants ?? 0),
|
|
43773
|
-
observedAt: refreshedAt
|
|
43774
|
-
}
|
|
43775
|
-
})
|
|
43776
|
-
});
|
|
43777
|
-
fetched += 1;
|
|
43778
|
-
}
|
|
43779
|
-
}).pipe(Effect.catch(() => Effect.sync(() => {
|
|
43780
|
-
failedSources.push(source.name);
|
|
43781
|
-
}))), { concurrency: FEED_FETCH_CONCURRENCY });
|
|
43782
|
-
yield* call("/feed/refreshed-at", {
|
|
43783
|
-
method: "PUT",
|
|
43784
|
-
body: JSON.stringify({ refreshedAt })
|
|
43785
|
-
});
|
|
43786
|
-
yield* call("/feed/lease", {
|
|
43787
|
-
method: "DELETE",
|
|
43788
|
-
body: JSON.stringify({ owner })
|
|
43789
|
-
});
|
|
43790
|
-
return {
|
|
43791
|
-
fetched,
|
|
43792
|
-
summarized,
|
|
43793
|
-
failedSources,
|
|
43794
|
-
refreshedAt
|
|
43795
|
-
};
|
|
43796
|
-
}),
|
|
43797
|
-
sources,
|
|
43798
|
-
upsertSource: (input) => call("/feed/sources", {
|
|
43987
|
+
const makeHubFeedStore = (linked) => ({
|
|
43988
|
+
listSources: () => call(linked, "/feed/sources"),
|
|
43989
|
+
upsertSource: (input) => call(linked, "/feed/sources", {
|
|
43799
43990
|
method: "PUT",
|
|
43800
43991
|
body: JSON.stringify(input)
|
|
43801
43992
|
}),
|
|
43802
|
-
deleteSource: (
|
|
43803
|
-
|
|
43993
|
+
deleteSource: (sourceId) => call(linked, `/feed/sources/${encodeURIComponent(sourceId)}`, { method: "DELETE" }),
|
|
43994
|
+
listArticles: () => call(linked, "/feed/articles"),
|
|
43995
|
+
putArticle: (id, write) => call(linked, `/feed/articles/${encodeURIComponent(id)}`, {
|
|
43996
|
+
method: "PUT",
|
|
43997
|
+
body: JSON.stringify(write)
|
|
43998
|
+
}),
|
|
43999
|
+
putEngagement: (articleId, engagement) => call(linked, `/feed/articles/${encodeURIComponent(articleId)}/engagement`, {
|
|
44000
|
+
method: "PUT",
|
|
44001
|
+
body: JSON.stringify(engagement)
|
|
44002
|
+
}).pipe(Effect.asVoid),
|
|
44003
|
+
markRead: (input) => call(linked, "/feed/read", {
|
|
43804
44004
|
method: "PUT",
|
|
43805
44005
|
body: JSON.stringify(input)
|
|
43806
44006
|
}),
|
|
43807
|
-
cleanup: (input) => call("/feed/cleanup", {
|
|
44007
|
+
cleanup: (input) => call(linked, "/feed/cleanup", {
|
|
43808
44008
|
method: "POST",
|
|
43809
44009
|
body: JSON.stringify(input)
|
|
43810
|
-
})
|
|
44010
|
+
}).pipe(Effect.map((result) => new FeedCleanupResult({ ...result }))),
|
|
44011
|
+
setRefreshedAt: (refreshedAt) => call(linked, "/feed/refreshed-at", {
|
|
44012
|
+
method: "PUT",
|
|
44013
|
+
body: JSON.stringify({ refreshedAt })
|
|
44014
|
+
}).pipe(Effect.asVoid),
|
|
44015
|
+
acquireLease: (owner, expiresAt) => call(linked, "/feed/lease", {
|
|
44016
|
+
method: "POST",
|
|
44017
|
+
body: JSON.stringify({
|
|
44018
|
+
owner,
|
|
44019
|
+
expiresAt
|
|
44020
|
+
})
|
|
44021
|
+
}).pipe(Effect.map((lease) => lease.acquired)),
|
|
44022
|
+
renewLease: (owner, expiresAt) => call(linked, "/feed/lease", {
|
|
44023
|
+
method: "PATCH",
|
|
44024
|
+
body: JSON.stringify({
|
|
44025
|
+
owner,
|
|
44026
|
+
expiresAt
|
|
44027
|
+
})
|
|
44028
|
+
}).pipe(Effect.asVoid),
|
|
44029
|
+
releaseLease: (owner) => call(linked, "/feed/lease", {
|
|
44030
|
+
method: "DELETE",
|
|
44031
|
+
body: JSON.stringify({ owner })
|
|
44032
|
+
}).pipe(Effect.asVoid)
|
|
44033
|
+
});
|
|
44034
|
+
const localContext = yield* Layer.build(makeHubPersistenceLive(path.join(config.stateDir, "feed.sqlite"))).pipe(Effect.orDie);
|
|
44035
|
+
const localStore = yield* makeLocalFeedStore.pipe(Effect.provide(localContext));
|
|
44036
|
+
const store = Effect.fn("FeedService.store")(function* () {
|
|
44037
|
+
const state = yield* hubLink.current;
|
|
44038
|
+
return Option.isSome(state.settings) ? makeHubFeedStore(state.settings.value) : localStore;
|
|
44039
|
+
});
|
|
44040
|
+
const sources = () => store().pipe(Effect.flatMap((feedStore) => feedStore.listSources()), Effect.map((items) => items.map(normalizeFeedSource)));
|
|
44041
|
+
const list = Effect.fn("FeedService.list")(function* (input) {
|
|
44042
|
+
const feedStore = yield* store();
|
|
44043
|
+
const result = normalizeFeedList(yield* feedStore.listArticles());
|
|
44044
|
+
const enabledSourceIds = new Set((yield* feedStore.listSources()).filter((source) => source.enabled).map((source) => source.id));
|
|
44045
|
+
const query = input.query?.trim().toLocaleLowerCase();
|
|
44046
|
+
const articles = result.articles.filter((article) => enabledSourceIds.has(article.sourceId)).filter((article) => !isPromotionalArticle(article.title)).filter((article) => input.sourceId === void 0 || article.sourceId === input.sourceId).filter((article) => input.category === void 0 || article.category === input.category).filter((article) => !input.unreadOnly || article.readAt === null).filter((article) => input.startDate === void 0 || article.publishedAt >= input.startDate).filter((article) => input.endDate === void 0 || article.publishedAt <= input.endDate).filter((article) => !query || `${article.title}\n${article.summary?.summary ?? ""}\n${article.summary?.highlights.join(" ") ?? ""}`.toLocaleLowerCase().includes(query)).toSorted((a, b) => input.sort === "oldest" ? a.publishedAt.localeCompare(b.publishedAt) : b.publishedAt.localeCompare(a.publishedAt));
|
|
44047
|
+
return new FeedListResult({
|
|
44048
|
+
...result,
|
|
44049
|
+
articles
|
|
44050
|
+
});
|
|
44051
|
+
});
|
|
44052
|
+
const refresh = Effect.fn("FeedService.refresh")(function* () {
|
|
44053
|
+
const feedStore = yield* store();
|
|
44054
|
+
const owner = crypto.randomUUID();
|
|
44055
|
+
const refreshedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
44056
|
+
if (!(yield* feedStore.acquireLease(owner, new Date(Date.now() + FEED_REFRESH_LEASE_MS).toISOString()))) return new FeedRefreshResult({
|
|
44057
|
+
fetched: 0,
|
|
44058
|
+
summarized: 0,
|
|
44059
|
+
failedSources: [],
|
|
44060
|
+
refreshedAt,
|
|
44061
|
+
alreadyRunning: true
|
|
44062
|
+
});
|
|
44063
|
+
const existing = normalizeFeedList(yield* feedStore.listArticles());
|
|
44064
|
+
const existingByUrl = new Map(existing.articles.map((article) => [article.canonicalUrl, article]));
|
|
44065
|
+
const enabled = (yield* sources()).filter((source) => source.enabled);
|
|
44066
|
+
let fetched = 0;
|
|
44067
|
+
let summarized = 0;
|
|
44068
|
+
const failedSources = [];
|
|
44069
|
+
yield* Effect.forEach(enabled, (source) => Effect.gen(function* () {
|
|
44070
|
+
const entries = source.kind === "hacker_news" ? yield* Effect.tryPromise({
|
|
44071
|
+
try: () => fetchHackerNewsFrontPage(),
|
|
44072
|
+
catch: () => fail("fetch_failed", `Could not fetch ${source.name}.`)
|
|
44073
|
+
}) : parseSyndicationFeed(yield* Effect.tryPromise({
|
|
44074
|
+
try: () => fetch(source.feedUrl).then((response) => {
|
|
44075
|
+
if (!response.ok) throw new Error(String(response.status));
|
|
44076
|
+
return response.text();
|
|
44077
|
+
}),
|
|
44078
|
+
catch: () => fail("fetch_failed", `Could not fetch ${source.name}.`)
|
|
44079
|
+
}));
|
|
44080
|
+
for (const entry of entries) {
|
|
44081
|
+
if (isPromotionalArticle(entry.title)) continue;
|
|
44082
|
+
const current = existingByUrl.get(entry.url);
|
|
44083
|
+
const hnId = source.kind === "hacker_news" ? hackerNewsItemId(entry.discussionUrl) : null;
|
|
44084
|
+
const hnEngagement = hnId === null || entry.points !== void 0 ? null : yield* Effect.tryPromise({
|
|
44085
|
+
try: () => fetch(`https://hacker-news.firebaseio.com/v0/item/${encodeURIComponent(hnId)}.json`).then((response) => response.ok ? response.json() : null),
|
|
44086
|
+
catch: () => null
|
|
44087
|
+
});
|
|
44088
|
+
const points = entry.points ?? hnEngagement?.score ?? null;
|
|
44089
|
+
const comments = entry.comments ?? hnEngagement?.descendants ?? null;
|
|
44090
|
+
const engagement = points === null && comments === null ? {
|
|
44091
|
+
points: null,
|
|
44092
|
+
comments: null,
|
|
44093
|
+
band: "unknown",
|
|
44094
|
+
observedAt: refreshedAt
|
|
44095
|
+
} : {
|
|
44096
|
+
points: points ?? 0,
|
|
44097
|
+
comments: comments ?? 0,
|
|
44098
|
+
band: hackerNewsEngagementBand(points ?? 0, comments ?? 0),
|
|
44099
|
+
observedAt: refreshedAt
|
|
44100
|
+
};
|
|
44101
|
+
let contentText = entry.content ? extractReadableArticleText(entry.content) : null;
|
|
44102
|
+
if (contentText === null) contentText = yield* Effect.tryPromise({
|
|
44103
|
+
try: () => fetch(entry.url).then((response) => response.ok ? response.text() : "").then(extractReadableArticleText),
|
|
44104
|
+
catch: () => null
|
|
44105
|
+
});
|
|
44106
|
+
const revision = contentText ?? `${entry.title}:${entry.publishedAt}`;
|
|
44107
|
+
if (current?.contentText === contentText && current.summary !== null) {
|
|
44108
|
+
if (engagement.band !== "unknown") yield* feedStore.putEngagement(current.id, engagement).pipe(Effect.ignore);
|
|
44109
|
+
continue;
|
|
44110
|
+
}
|
|
44111
|
+
const id = current?.id ?? FeedArticleId.make(crypto.randomUUID());
|
|
44112
|
+
let summary = null;
|
|
44113
|
+
let status = "summary_unavailable";
|
|
44114
|
+
let articleCategory = null;
|
|
44115
|
+
if (contentText !== null) {
|
|
44116
|
+
const instances = yield* providers.listInstances;
|
|
44117
|
+
const candidates = [{
|
|
44118
|
+
instance: instances.find((instance) => instance.driverKind === "codex" && instance.enabled),
|
|
44119
|
+
model: FEED_MODEL
|
|
44120
|
+
}, {
|
|
44121
|
+
instance: instances.find((instance) => instance.driverKind === "claudeAgent" && instance.enabled),
|
|
44122
|
+
model: FEED_FALLBACK_MODEL
|
|
44123
|
+
}];
|
|
44124
|
+
for (const candidate of candidates) {
|
|
44125
|
+
if (!candidate.instance) continue;
|
|
44126
|
+
const generated = yield* candidate.instance.textGeneration.generatePrContent({
|
|
44127
|
+
cwd: process.cwd(),
|
|
44128
|
+
baseBranch: "feed",
|
|
44129
|
+
headBranch: "article",
|
|
44130
|
+
commitSummary: entry.title,
|
|
44131
|
+
diffSummary: `Summarize this article for a dense news feed. Return a concise summary in the body. Start the body with exactly one line "Category: <name>" where <name> is one of: ${FEED_CATEGORIES.join(", ")}.`,
|
|
44132
|
+
diffPatch: contentText.slice(0, 24e3),
|
|
44133
|
+
modelSelection: {
|
|
44134
|
+
instanceId: ProviderInstanceId.make(candidate.instance.instanceId),
|
|
44135
|
+
model: candidate.model
|
|
44136
|
+
}
|
|
44137
|
+
}).pipe(Effect.option);
|
|
44138
|
+
if (Option.isNone(generated)) continue;
|
|
44139
|
+
const categorized = extractGeneratedCategory(generated.value.body);
|
|
44140
|
+
const normalized = normalizeGeneratedSummary(categorized.body);
|
|
44141
|
+
if (!normalized.summary) continue;
|
|
44142
|
+
articleCategory = categorized.category;
|
|
44143
|
+
summary = {
|
|
44144
|
+
modelSelection: `${candidate.instance.instanceId}:${candidate.model}`,
|
|
44145
|
+
summary: normalized.summary,
|
|
44146
|
+
highlights: normalized.highlights,
|
|
44147
|
+
references: [entry.url],
|
|
44148
|
+
summarizedAt: refreshedAt
|
|
44149
|
+
};
|
|
44150
|
+
status = "ready";
|
|
44151
|
+
summarized += 1;
|
|
44152
|
+
break;
|
|
44153
|
+
}
|
|
44154
|
+
if (summary === null) status = "summary_failed";
|
|
44155
|
+
}
|
|
44156
|
+
yield* feedStore.putArticle(id, {
|
|
44157
|
+
sourceId: source.id,
|
|
44158
|
+
category: articleCategory,
|
|
44159
|
+
canonicalUrl: entry.url,
|
|
44160
|
+
discussionUrl: hnId ? hackerNewsDiscussionUrl(hnId) : null,
|
|
44161
|
+
title: entry.title,
|
|
44162
|
+
author: entry.author,
|
|
44163
|
+
publishedAt: entry.publishedAt,
|
|
44164
|
+
fetchedAt: refreshedAt,
|
|
44165
|
+
contentText,
|
|
44166
|
+
status,
|
|
44167
|
+
contentRevision: revision,
|
|
44168
|
+
summary,
|
|
44169
|
+
engagement
|
|
44170
|
+
});
|
|
44171
|
+
fetched += 1;
|
|
44172
|
+
yield* feedStore.renewLease(owner, new Date(Date.now() + FEED_REFRESH_LEASE_MS).toISOString()).pipe(Effect.ignore);
|
|
44173
|
+
}
|
|
44174
|
+
}).pipe(Effect.catch(() => Effect.sync(() => {
|
|
44175
|
+
failedSources.push(source.name);
|
|
44176
|
+
}))), { concurrency: FEED_FETCH_CONCURRENCY });
|
|
44177
|
+
yield* feedStore.setRefreshedAt(refreshedAt);
|
|
44178
|
+
yield* feedStore.releaseLease(owner);
|
|
44179
|
+
return new FeedRefreshResult({
|
|
44180
|
+
fetched,
|
|
44181
|
+
summarized,
|
|
44182
|
+
failedSources,
|
|
44183
|
+
refreshedAt
|
|
44184
|
+
});
|
|
44185
|
+
});
|
|
44186
|
+
const morningRefreshDaemon = (hour) => Effect.gen(function* () {
|
|
44187
|
+
while (true) {
|
|
44188
|
+
const now = /* @__PURE__ */ new Date();
|
|
44189
|
+
const next = new Date(now);
|
|
44190
|
+
next.setHours(hour, 0, 0, 0);
|
|
44191
|
+
if (next.getTime() <= now.getTime()) next.setDate(next.getDate() + 1);
|
|
44192
|
+
yield* Effect.sleep(Duration.millis(next.getTime() - now.getTime()));
|
|
44193
|
+
yield* refresh().pipe(Effect.ignore);
|
|
44194
|
+
}
|
|
44195
|
+
});
|
|
44196
|
+
if (morningRefreshHour !== null) yield* Effect.forkScoped(morningRefreshDaemon(morningRefreshHour));
|
|
44197
|
+
return {
|
|
44198
|
+
list,
|
|
44199
|
+
refresh,
|
|
44200
|
+
sources,
|
|
44201
|
+
upsertSource: (input) => store().pipe(Effect.flatMap((feedStore) => feedStore.upsertSource(input)), Effect.map(normalizeFeedSource)),
|
|
44202
|
+
deleteSource: (input) => store().pipe(Effect.flatMap((feedStore) => feedStore.deleteSource(input.sourceId))),
|
|
44203
|
+
markRead: (input) => store().pipe(Effect.flatMap((feedStore) => feedStore.markRead(input))),
|
|
44204
|
+
cleanup: (input) => store().pipe(Effect.flatMap((feedStore) => feedStore.cleanup(input)))
|
|
43811
44205
|
};
|
|
43812
44206
|
});
|
|
43813
44207
|
const unavailable = () => Effect.fail(fail("hub_unavailable", "Feed service is unavailable."));
|
|
@@ -65991,6 +66385,58 @@ function classifyToolItemType(toolName) {
|
|
|
65991
66385
|
if (normalized.includes("image")) return "image_view";
|
|
65992
66386
|
return "dynamic_tool_call";
|
|
65993
66387
|
}
|
|
66388
|
+
const FILE_CHANGE_PATCH_CONTEXT_LINES = 3;
|
|
66389
|
+
/** Oversized patches are dropped rather than shipped with every timeline row. */
|
|
66390
|
+
const FILE_CHANGE_PATCH_MAX_LENGTH = 2e4;
|
|
66391
|
+
function ensureTrailingNewline(value) {
|
|
66392
|
+
return value.endsWith("\n") ? value : `${value}\n`;
|
|
66393
|
+
}
|
|
66394
|
+
function unifiedEditPatch(filePath, oldText, newText) {
|
|
66395
|
+
if (oldText === newText) return;
|
|
66396
|
+
const patch = structuredPatch(`a/${filePath}`, `b/${filePath}`, ensureTrailingNewline(oldText), ensureTrailingNewline(newText), void 0, void 0, { context: FILE_CHANGE_PATCH_CONTEXT_LINES });
|
|
66397
|
+
if (patch.hunks.length === 0) return;
|
|
66398
|
+
const lines = [`--- a/${filePath}`, `+++ b/${filePath}`];
|
|
66399
|
+
for (const hunk of patch.hunks) {
|
|
66400
|
+
lines.push(`@@ -${hunk.oldStart},${hunk.oldLines} +${hunk.newStart},${hunk.newLines} @@`);
|
|
66401
|
+
for (const line of hunk.lines) lines.push(line);
|
|
66402
|
+
}
|
|
66403
|
+
return `${lines.join("\n")}\n`;
|
|
66404
|
+
}
|
|
66405
|
+
/**
|
|
66406
|
+
* A unified diff for the edits one file-change tool call carries. Only
|
|
66407
|
+
* Edit-style inputs produce one: `old_string`/`new_string`, or a
|
|
66408
|
+
* MultiEdit-style `edits` array of such pairs. Write ships full content with
|
|
66409
|
+
* no previous version to diff against, so its row keeps the plain path list.
|
|
66410
|
+
*
|
|
66411
|
+
* The snippet-local diff is exact for the strings the call replaces; hunk line
|
|
66412
|
+
* numbers are relative to the snippet rather than the file.
|
|
66413
|
+
*/
|
|
66414
|
+
function fileChangePatchForToolInput(itemType, input) {
|
|
66415
|
+
if (itemType !== "file_change") return;
|
|
66416
|
+
const filePath = readString(input.file_path) ?? readString(input.path);
|
|
66417
|
+
if (!filePath) return;
|
|
66418
|
+
const pairs = [];
|
|
66419
|
+
if (typeof input.old_string === "string" && typeof input.new_string === "string") pairs.push({
|
|
66420
|
+
oldText: input.old_string,
|
|
66421
|
+
newText: input.new_string
|
|
66422
|
+
});
|
|
66423
|
+
else if (Array.isArray(input.edits)) for (const edit of input.edits) {
|
|
66424
|
+
if (edit === null || typeof edit !== "object") continue;
|
|
66425
|
+
const editRecord = edit;
|
|
66426
|
+
if (typeof editRecord.old_string === "string" && typeof editRecord.new_string === "string") pairs.push({
|
|
66427
|
+
oldText: editRecord.old_string,
|
|
66428
|
+
newText: editRecord.new_string
|
|
66429
|
+
});
|
|
66430
|
+
}
|
|
66431
|
+
const sections = [];
|
|
66432
|
+
for (const pair of pairs) {
|
|
66433
|
+
const section = unifiedEditPatch(filePath, pair.oldText, pair.newText);
|
|
66434
|
+
if (section !== void 0) sections.push(section);
|
|
66435
|
+
}
|
|
66436
|
+
if (sections.length === 0) return;
|
|
66437
|
+
const patch = sections.join("");
|
|
66438
|
+
return patch.length <= FILE_CHANGE_PATCH_MAX_LENGTH ? patch : void 0;
|
|
66439
|
+
}
|
|
65994
66440
|
function isReadOnlyToolName(toolName) {
|
|
65995
66441
|
const normalized = toolName.toLowerCase();
|
|
65996
66442
|
return normalized === "read" || normalized.includes("read file") || normalized.includes("view") || normalized.includes("grep") || normalized.includes("glob") || normalized.includes("search");
|
|
@@ -66866,6 +67312,7 @@ const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* (claudeSettin
|
|
|
66866
67312
|
return;
|
|
66867
67313
|
}
|
|
66868
67314
|
for (const [index, tool] of context.inFlightTools.entries()) {
|
|
67315
|
+
const flushedInputPatch = fileChangePatchForToolInput(tool.itemType, tool.input);
|
|
66869
67316
|
const toolStamp = yield* makeEventStamp();
|
|
66870
67317
|
yield* offerRuntimeEvent({
|
|
66871
67318
|
type: "item.completed",
|
|
@@ -66883,7 +67330,8 @@ const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* (claudeSettin
|
|
|
66883
67330
|
data: {
|
|
66884
67331
|
toolCallId: tool.itemId,
|
|
66885
67332
|
toolName: tool.toolName,
|
|
66886
|
-
input: tool.input
|
|
67333
|
+
input: tool.input,
|
|
67334
|
+
...flushedInputPatch !== void 0 ? { patch: flushedInputPatch } : {}
|
|
66887
67335
|
}
|
|
66888
67336
|
},
|
|
66889
67337
|
providerRefs: nativeProviderRefs(context, { providerItemId: tool.itemId }),
|
|
@@ -67128,6 +67576,7 @@ const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* (claudeSettin
|
|
|
67128
67576
|
lastEmittedInputFingerprint: nextFingerprint
|
|
67129
67577
|
};
|
|
67130
67578
|
context.inFlightTools.set(event.index, nextTool);
|
|
67579
|
+
const streamedInputPatch = fileChangePatchForToolInput(nextTool.itemType, nextTool.input);
|
|
67131
67580
|
const stamp = yield* makeEventStamp();
|
|
67132
67581
|
yield* offerRuntimeEvent({
|
|
67133
67582
|
type: "item.updated",
|
|
@@ -67145,7 +67594,8 @@ const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* (claudeSettin
|
|
|
67145
67594
|
data: {
|
|
67146
67595
|
toolCallId: nextTool.itemId,
|
|
67147
67596
|
toolName: nextTool.toolName,
|
|
67148
|
-
input: nextTool.input
|
|
67597
|
+
input: nextTool.input,
|
|
67598
|
+
...streamedInputPatch !== void 0 ? { patch: streamedInputPatch } : {}
|
|
67149
67599
|
}
|
|
67150
67600
|
},
|
|
67151
67601
|
providerRefs: nativeProviderRefs(context, { providerItemId: nextTool.itemId }),
|
|
@@ -67198,6 +67648,7 @@ const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* (claudeSettin
|
|
|
67198
67648
|
...inputFingerprint ? { lastEmittedInputFingerprint: inputFingerprint } : {}
|
|
67199
67649
|
};
|
|
67200
67650
|
context.inFlightTools.set(index, tool);
|
|
67651
|
+
const startedInputPatch = fileChangePatchForToolInput(tool.itemType, toolInput);
|
|
67201
67652
|
const stamp = yield* makeEventStamp();
|
|
67202
67653
|
yield* offerRuntimeEvent({
|
|
67203
67654
|
type: "item.started",
|
|
@@ -67215,7 +67666,8 @@ const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* (claudeSettin
|
|
|
67215
67666
|
data: {
|
|
67216
67667
|
toolCallId: tool.itemId,
|
|
67217
67668
|
toolName: tool.toolName,
|
|
67218
|
-
input: toolInput
|
|
67669
|
+
input: toolInput,
|
|
67670
|
+
...startedInputPatch !== void 0 ? { patch: startedInputPatch } : {}
|
|
67219
67671
|
}
|
|
67220
67672
|
},
|
|
67221
67673
|
providerRefs: nativeProviderRefs(context, { providerItemId: tool.itemId }),
|
|
@@ -67250,10 +67702,12 @@ const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* (claudeSettin
|
|
|
67250
67702
|
const [index, tool] = toolEntry;
|
|
67251
67703
|
const itemStatus = toolResult.isError ? "failed" : "completed";
|
|
67252
67704
|
const toolUseResult = readClaudeToolUseResult(message);
|
|
67705
|
+
const resultInputPatch = fileChangePatchForToolInput(tool.itemType, tool.input);
|
|
67253
67706
|
const toolData = {
|
|
67254
67707
|
toolCallId: tool.itemId,
|
|
67255
67708
|
toolName: tool.toolName,
|
|
67256
67709
|
input: tool.input,
|
|
67710
|
+
...resultInputPatch !== void 0 ? { patch: resultInputPatch } : {},
|
|
67257
67711
|
result: toolResult.block
|
|
67258
67712
|
};
|
|
67259
67713
|
const updatedStamp = yield* makeEventStamp();
|