@chronicle.app/pinboard 0.3.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/LICENSE +21 -0
- package/README.md +62 -0
- package/dist/connectors/PinboardExtractor.d.ts +40 -0
- package/dist/connectors/PinboardExtractor.js +95 -0
- package/dist/connectors/PinboardTransformer.d.ts +10 -0
- package/dist/connectors/PinboardTransformer.js +85 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +2 -0
- package/dist/utils/PinboardProxy.d.ts +32 -0
- package/dist/utils/PinboardProxy.js +81 -0
- package/package.json +75 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Andrew Louis
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# @chronicle.app/pinboard
|
|
2
|
+
|
|
3
|
+
Chronicle plugin for pinboard
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @chronicle.app/pinboard
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
### Basic extraction
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
chronicle extract pinboard --api-key YOUR_API_KEY
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
### With date filtering
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
chronicle extract pinboard \
|
|
23
|
+
--api-key YOUR_API_KEY \
|
|
24
|
+
--since 2024-01-01 \
|
|
25
|
+
--until 2024-12-31 \
|
|
26
|
+
--limit 1000
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Configuration
|
|
30
|
+
|
|
31
|
+
The extractor supports the following options:
|
|
32
|
+
|
|
33
|
+
- `--api-key`: API key for authentication, in Pinboard's `username:TOKEN`
|
|
34
|
+
form. Without it, the key stored by `chronicle auth set pinboard` is used.
|
|
35
|
+
- `--tag-blacklist`: Tags to exclude from extraction (default: `twitterfavs`)
|
|
36
|
+
- `--since`: Extract records from this date onwards
|
|
37
|
+
- `--until`: Extract records up to this date
|
|
38
|
+
- `--limit`: Maximum number of records to extract
|
|
39
|
+
|
|
40
|
+
## Development
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
# Install dependencies
|
|
44
|
+
npm install
|
|
45
|
+
|
|
46
|
+
# Build the plugin
|
|
47
|
+
npm run build
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## API Endpoints
|
|
51
|
+
|
|
52
|
+
TODO: Document the API endpoints used and response formats.
|
|
53
|
+
|
|
54
|
+
## Schema
|
|
55
|
+
|
|
56
|
+
TODO: Document what Chronicle schema types are generated from this data source.
|
|
57
|
+
|
|
58
|
+
## Tests
|
|
59
|
+
|
|
60
|
+
Tests serve synthetic posts from a local HTTP server on 127.0.0.1 and pass a
|
|
61
|
+
fake API key. They never call Pinboard or read stored credentials: the
|
|
62
|
+
credential store points at an empty temporary directory.
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { Extractor, Record } from '@chronicle.app/etl';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import PinboardTransformer from './PinboardTransformer.js';
|
|
4
|
+
export declare class PinboardExtractor extends Extractor<typeof PinboardExtractor> {
|
|
5
|
+
static source: string;
|
|
6
|
+
static description: string;
|
|
7
|
+
static delivery: "api";
|
|
8
|
+
static strategy: string;
|
|
9
|
+
static recordTypes: string[];
|
|
10
|
+
static default: boolean;
|
|
11
|
+
static temporality: "snapshot";
|
|
12
|
+
static defaultTransformer: typeof PinboardTransformer;
|
|
13
|
+
static schema: z.ZodObject<{
|
|
14
|
+
since: z.ZodOptional<z.ZodDate>;
|
|
15
|
+
until: z.ZodOptional<z.ZodDate>;
|
|
16
|
+
limit: z.ZodOptional<z.ZodNumber>;
|
|
17
|
+
} & {
|
|
18
|
+
tagBlacklist: z.ZodDefault<z.ZodOptional<z.ZodArray<z.ZodString, "many">>>;
|
|
19
|
+
apiKey: z.ZodOptional<z.ZodString>;
|
|
20
|
+
}, "strip", z.ZodTypeAny, {
|
|
21
|
+
tagBlacklist: string[];
|
|
22
|
+
apiKey?: string | undefined;
|
|
23
|
+
since?: Date | undefined;
|
|
24
|
+
until?: Date | undefined;
|
|
25
|
+
limit?: number | undefined;
|
|
26
|
+
}, {
|
|
27
|
+
apiKey?: string | undefined;
|
|
28
|
+
tagBlacklist?: string[] | undefined;
|
|
29
|
+
since?: Date | undefined;
|
|
30
|
+
until?: Date | undefined;
|
|
31
|
+
limit?: number | undefined;
|
|
32
|
+
}>;
|
|
33
|
+
private proxy;
|
|
34
|
+
/** The bookmark's `hash` — Pinboard's own id for the saved url, identical through any tool. */
|
|
35
|
+
keyOf(record: Record): string | null;
|
|
36
|
+
setup(): Promise<void>;
|
|
37
|
+
extract(): AsyncGenerator<Record>;
|
|
38
|
+
determineCount(): Promise<number | null>;
|
|
39
|
+
}
|
|
40
|
+
//# sourceMappingURL=PinboardExtractor.d.ts.map
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { Extractor } from '@chronicle.app/etl';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import PinboardTransformer from './PinboardTransformer.js';
|
|
4
|
+
import PinboardProxy from '../utils/PinboardProxy.js';
|
|
5
|
+
export class PinboardExtractor extends Extractor {
|
|
6
|
+
static source = 'pinboard';
|
|
7
|
+
static description = 'Bookmarks';
|
|
8
|
+
static delivery = 'api';
|
|
9
|
+
static strategy = 'api';
|
|
10
|
+
static recordTypes = ['bookmarks'];
|
|
11
|
+
static default = true;
|
|
12
|
+
// `/posts/all` re-reports every bookmark with its latest title, description,
|
|
13
|
+
// and tags each run, so it's a state re-read, not an append-only feed:
|
|
14
|
+
// attributes are sighted at read time rather than back-dated to the save
|
|
15
|
+
// instant, letting an edit (a retitled or retagged bookmark) supersede cleanly
|
|
16
|
+
// instead of colliding with the old value at that timestamp. The BookmarkAction
|
|
17
|
+
// keeps its true save date on the timeline. Pinboard is a live API, so each
|
|
18
|
+
// read is a genuine sighting and the default run-start asOfTime is correct.
|
|
19
|
+
// See Extractor.temporality.
|
|
20
|
+
static temporality = 'snapshot';
|
|
21
|
+
static defaultTransformer = PinboardTransformer;
|
|
22
|
+
static schema = Extractor.schema.extend({
|
|
23
|
+
tagBlacklist: z
|
|
24
|
+
.array(z.string())
|
|
25
|
+
.optional()
|
|
26
|
+
.default(['twitterfavs'])
|
|
27
|
+
.describe('Tags to exclude from extraction'),
|
|
28
|
+
apiKey: z
|
|
29
|
+
.string()
|
|
30
|
+
.optional()
|
|
31
|
+
.describe('Pinboard API key for authentication (auto-retrieved from stored credentials if not provided)'),
|
|
32
|
+
});
|
|
33
|
+
proxy;
|
|
34
|
+
/** The bookmark's `hash` — Pinboard's own id for the saved url, identical through any tool. */
|
|
35
|
+
keyOf(record) {
|
|
36
|
+
return record.data.hash ?? null;
|
|
37
|
+
}
|
|
38
|
+
async setup() {
|
|
39
|
+
const config = this.config;
|
|
40
|
+
this.proxy = new PinboardProxy(config.apiKey);
|
|
41
|
+
await this.proxy.initialize();
|
|
42
|
+
}
|
|
43
|
+
async *extract() {
|
|
44
|
+
const config = this.config;
|
|
45
|
+
try {
|
|
46
|
+
const posts = await this.proxy.getAllPosts({
|
|
47
|
+
fromdt: config.since,
|
|
48
|
+
todt: config.until,
|
|
49
|
+
});
|
|
50
|
+
this.logger.info(`Retrieved ${posts.length} posts from Pinboard`);
|
|
51
|
+
let totalCount = 0;
|
|
52
|
+
for (const post of posts) {
|
|
53
|
+
// Apply date filtering
|
|
54
|
+
const postDate = new Date(post.time);
|
|
55
|
+
if (config.since && postDate < config.since)
|
|
56
|
+
continue;
|
|
57
|
+
if (config.until && postDate > config.until)
|
|
58
|
+
continue;
|
|
59
|
+
// Apply tag blacklist
|
|
60
|
+
const postTags = new Set(post.tags.split(' ').filter((tag) => tag.length > 0));
|
|
61
|
+
if (config.tagBlacklist &&
|
|
62
|
+
config.tagBlacklist.some(blacklistedTag => postTags.has(blacklistedTag))) {
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
// Apply limit
|
|
66
|
+
if (this.shouldStopExtracting(totalCount)) {
|
|
67
|
+
break;
|
|
68
|
+
}
|
|
69
|
+
const username = this.proxy.getUsername();
|
|
70
|
+
yield this.createRecord(post, {
|
|
71
|
+
recordType: 'bookmarks',
|
|
72
|
+
username,
|
|
73
|
+
});
|
|
74
|
+
totalCount++;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
catch (error) {
|
|
78
|
+
this.logger.error('Error fetching Pinboard posts');
|
|
79
|
+
throw error;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
async determineCount() {
|
|
83
|
+
try {
|
|
84
|
+
const config = this.config;
|
|
85
|
+
const posts = await this.proxy.getAllPosts({
|
|
86
|
+
fromdt: config.since,
|
|
87
|
+
todt: config.until,
|
|
88
|
+
});
|
|
89
|
+
return posts.length;
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { ChronicleTransformer, Record } from '@chronicle.app/etl';
|
|
2
|
+
import { ActionAndChildren } from '@chronicle.app/schema';
|
|
3
|
+
export default class PinboardTransformer extends ChronicleTransformer {
|
|
4
|
+
transform(record: Record): Promise<ActionAndChildren[]>;
|
|
5
|
+
private buildBookmarkActions;
|
|
6
|
+
private buildWebpage;
|
|
7
|
+
private buildUser;
|
|
8
|
+
private buildTags;
|
|
9
|
+
}
|
|
10
|
+
//# sourceMappingURL=PinboardTransformer.d.ts.map
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { ChronicleTransformer } from '@chronicle.app/etl';
|
|
2
|
+
const SOURCE = 'pinboard';
|
|
3
|
+
// Pinboard returns empty strings for an absent description/extended note. The
|
|
4
|
+
// schema treats optional fields as absent-or-undefined, so coalesce to a trimmed
|
|
5
|
+
// string or undefined and omit the field when there's nothing to set.
|
|
6
|
+
function str(value) {
|
|
7
|
+
if (typeof value !== 'string')
|
|
8
|
+
return undefined;
|
|
9
|
+
const trimmed = value.trim();
|
|
10
|
+
return trimmed.length > 0 ? trimmed : undefined;
|
|
11
|
+
}
|
|
12
|
+
export default class PinboardTransformer extends ChronicleTransformer {
|
|
13
|
+
async transform(record) {
|
|
14
|
+
const actions = [];
|
|
15
|
+
if (record.extraction.recordType === 'bookmarks') {
|
|
16
|
+
actions.push(...this.buildBookmarkActions(record));
|
|
17
|
+
}
|
|
18
|
+
return actions;
|
|
19
|
+
}
|
|
20
|
+
buildBookmarkActions(record) {
|
|
21
|
+
const { data } = record;
|
|
22
|
+
const user = this.buildUser(record);
|
|
23
|
+
const tags = this.buildTags(record);
|
|
24
|
+
const webpage = this.buildWebpage(record, tags);
|
|
25
|
+
const bookmarkAction = {
|
|
26
|
+
'@type': 'BookmarkAction',
|
|
27
|
+
// The save itself is a discrete event: keep its true date on the timeline
|
|
28
|
+
// even though the snapshot sights the bookmark's mutable attributes now.
|
|
29
|
+
timestamp: new Date(data.time),
|
|
30
|
+
'@key': ['@type', 'source', 'sourceId'],
|
|
31
|
+
source: SOURCE,
|
|
32
|
+
sourceId: data.hash,
|
|
33
|
+
agent: user,
|
|
34
|
+
object: webpage,
|
|
35
|
+
};
|
|
36
|
+
return [bookmarkAction];
|
|
37
|
+
}
|
|
38
|
+
buildWebpage(record, tags) {
|
|
39
|
+
const { data } = record;
|
|
40
|
+
const webpage = {
|
|
41
|
+
'@type': 'Entity',
|
|
42
|
+
// Keyed to this source, not the bare `{url}` hub, because of the
|
|
43
|
+
// `@asserts` below: a completeness claim can only speak for its own
|
|
44
|
+
// source. On the shared hub, clearing the note here would also close
|
|
45
|
+
// another source's description for the same url (and their next crawl
|
|
46
|
+
// would close ours) — an invented history of values churning. The merge
|
|
47
|
+
// still happens: the `url` field mints the `{url}` hub regardless of key.
|
|
48
|
+
'@key': ['url', 'source'],
|
|
49
|
+
source: SOURCE,
|
|
50
|
+
url: data.href,
|
|
51
|
+
...(str(data.description) && { name: str(data.description) }),
|
|
52
|
+
...(str(data.extended) && { description: str(data.extended) }),
|
|
53
|
+
...(tags.length > 0 && { about: tags }),
|
|
54
|
+
};
|
|
55
|
+
// `description` and `about` can be cleared on a later snapshot (the note
|
|
56
|
+
// emptied, all tags removed); the `'*'` default only covers predicates still
|
|
57
|
+
// present, so list them explicitly to close the old values on re-read.
|
|
58
|
+
webpage['@asserts'] = ['description', 'about'];
|
|
59
|
+
return webpage;
|
|
60
|
+
}
|
|
61
|
+
buildUser(record) {
|
|
62
|
+
const username = record.context.username;
|
|
63
|
+
return {
|
|
64
|
+
'@type': 'Person',
|
|
65
|
+
'@key': ['@type', 'source', 'handle'],
|
|
66
|
+
source: SOURCE,
|
|
67
|
+
handle: username,
|
|
68
|
+
name: username,
|
|
69
|
+
url: `https://pinboard.in/u:${username}/`,
|
|
70
|
+
sameAs: ['@me'],
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
buildTags(record) {
|
|
74
|
+
const { data } = record;
|
|
75
|
+
if (!data.tags)
|
|
76
|
+
return [];
|
|
77
|
+
const tagNames = data.tags.split(' ').filter((tag) => tag.length > 0);
|
|
78
|
+
return tagNames.map((tagName) => ({
|
|
79
|
+
'@type': 'Tag',
|
|
80
|
+
'@key': ['@type', 'source', 'handle'],
|
|
81
|
+
source: SOURCE,
|
|
82
|
+
handle: tagName,
|
|
83
|
+
}));
|
|
84
|
+
}
|
|
85
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { ApiProxy } from '@chronicle.app/etl';
|
|
2
|
+
export interface PinboardPost {
|
|
3
|
+
href: string;
|
|
4
|
+
description: string;
|
|
5
|
+
extended: string;
|
|
6
|
+
meta: string;
|
|
7
|
+
hash: string;
|
|
8
|
+
time: string;
|
|
9
|
+
shared: string;
|
|
10
|
+
toread: string;
|
|
11
|
+
tags: string;
|
|
12
|
+
}
|
|
13
|
+
export default class PinboardProxy extends ApiProxy {
|
|
14
|
+
private authToken;
|
|
15
|
+
private username;
|
|
16
|
+
getAccessToken(): string;
|
|
17
|
+
getUsername(): string;
|
|
18
|
+
constructor(accessToken?: string);
|
|
19
|
+
initialize(): Promise<void>;
|
|
20
|
+
private authParams;
|
|
21
|
+
getAllPosts(options?: {
|
|
22
|
+
fromdt?: Date;
|
|
23
|
+
todt?: Date;
|
|
24
|
+
tag?: string;
|
|
25
|
+
}): Promise<PinboardPost[]>;
|
|
26
|
+
getRecentPosts(options?: {
|
|
27
|
+
tag?: string;
|
|
28
|
+
count?: number;
|
|
29
|
+
}): Promise<PinboardPost[]>;
|
|
30
|
+
getAllTags(): Promise<Record<string, number>>;
|
|
31
|
+
}
|
|
32
|
+
//# sourceMappingURL=PinboardProxy.d.ts.map
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { ApiProxy } from '@chronicle.app/etl';
|
|
2
|
+
import { resolveCredentials } from '@chronicle.app/auth';
|
|
3
|
+
export default class PinboardProxy extends ApiProxy {
|
|
4
|
+
// Pinboard authenticates with an `auth_token` query parameter, not a bearer
|
|
5
|
+
// header, so the token lives here and the base's accessToken stays unset.
|
|
6
|
+
authToken;
|
|
7
|
+
username;
|
|
8
|
+
getAccessToken() {
|
|
9
|
+
return this.authToken;
|
|
10
|
+
}
|
|
11
|
+
getUsername() {
|
|
12
|
+
return this.username;
|
|
13
|
+
}
|
|
14
|
+
constructor(accessToken) {
|
|
15
|
+
super({
|
|
16
|
+
baseURL: 'https://api.pinboard.in',
|
|
17
|
+
headers: {
|
|
18
|
+
'User-Agent': 'Chronicle/1.0',
|
|
19
|
+
},
|
|
20
|
+
});
|
|
21
|
+
this.authToken = accessToken || '';
|
|
22
|
+
this.username = '';
|
|
23
|
+
}
|
|
24
|
+
async initialize() {
|
|
25
|
+
// `auth set` stores the token under the canonical `accessToken`;
|
|
26
|
+
// `apiKey` is kept as a fallback for any older stored credential.
|
|
27
|
+
const { accessToken } = await resolveCredentials('pinboard', {
|
|
28
|
+
accessToken: { from: ['accessToken', 'apiKey'] },
|
|
29
|
+
}, {
|
|
30
|
+
overrides: { accessToken: this.authToken || undefined },
|
|
31
|
+
errorMessage: 'Pinboard API key is required. Set it via --api-key or store in credentials.',
|
|
32
|
+
});
|
|
33
|
+
this.authToken = accessToken;
|
|
34
|
+
// Extract username from API key (format is username:token)
|
|
35
|
+
this.username = this.authToken.split(':')[0] || '';
|
|
36
|
+
}
|
|
37
|
+
authParams() {
|
|
38
|
+
return new URLSearchParams({
|
|
39
|
+
auth_token: this.authToken,
|
|
40
|
+
format: 'json',
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
async getAllPosts(options = {}) {
|
|
44
|
+
const params = this.authParams();
|
|
45
|
+
params.append('meta', 'yes');
|
|
46
|
+
if (options.fromdt) {
|
|
47
|
+
params.append('fromdt', options.fromdt.toISOString());
|
|
48
|
+
}
|
|
49
|
+
if (options.todt) {
|
|
50
|
+
params.append('todt', options.todt.toISOString());
|
|
51
|
+
}
|
|
52
|
+
if (options.tag) {
|
|
53
|
+
params.append('tag', options.tag);
|
|
54
|
+
}
|
|
55
|
+
return this.request({
|
|
56
|
+
url: `/v1/posts/all?${params.toString()}`,
|
|
57
|
+
method: 'GET',
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
async getRecentPosts(options = {}) {
|
|
61
|
+
const params = this.authParams();
|
|
62
|
+
params.append('meta', 'yes');
|
|
63
|
+
if (options.tag) {
|
|
64
|
+
params.append('tag', options.tag);
|
|
65
|
+
}
|
|
66
|
+
if (options.count) {
|
|
67
|
+
params.append('count', options.count.toString());
|
|
68
|
+
}
|
|
69
|
+
const data = await this.request({
|
|
70
|
+
url: `/v1/posts/recent?${params.toString()}`,
|
|
71
|
+
method: 'GET',
|
|
72
|
+
});
|
|
73
|
+
return data.posts;
|
|
74
|
+
}
|
|
75
|
+
async getAllTags() {
|
|
76
|
+
return this.request({
|
|
77
|
+
url: `/v1/tags/get?${this.authParams().toString()}`,
|
|
78
|
+
method: 'GET',
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@chronicle.app/pinboard",
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"main": "./dist/index.js",
|
|
5
|
+
"types": "./dist/index.d.ts",
|
|
6
|
+
"files": [
|
|
7
|
+
"dist/**/*.js",
|
|
8
|
+
"dist/**/*.d.ts"
|
|
9
|
+
],
|
|
10
|
+
"scripts": {
|
|
11
|
+
"build": "tsc -b tsconfig.lib.json",
|
|
12
|
+
"typecheck": "tsc -p tsconfig.json",
|
|
13
|
+
"lint": "eslint src --ext .ts,.js",
|
|
14
|
+
"test": "node --test src/*.test.js"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"chronicle",
|
|
18
|
+
"pinboard"
|
|
19
|
+
],
|
|
20
|
+
"author": "Andrew Louis",
|
|
21
|
+
"license": "MIT",
|
|
22
|
+
"type": "module",
|
|
23
|
+
"description": "Chronicle plugin for bookmarks from Pinboard",
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"@chronicle.app/etl": "0.3.0",
|
|
26
|
+
"@chronicle.app/schema": "0.3.0",
|
|
27
|
+
"@chronicle.app/tsconfig": "0.3.0",
|
|
28
|
+
"@types/node": "^22.10.1",
|
|
29
|
+
"typescript": "^5.7.2"
|
|
30
|
+
},
|
|
31
|
+
"homepage": "https://chronicle.app",
|
|
32
|
+
"repository": {
|
|
33
|
+
"type": "git",
|
|
34
|
+
"url": "https://github.com/chronicle-app/chronicle.git",
|
|
35
|
+
"directory": "plugins/pinboard"
|
|
36
|
+
},
|
|
37
|
+
"publishConfig": {
|
|
38
|
+
"access": "public",
|
|
39
|
+
"registry": "https://registry.npmjs.org/"
|
|
40
|
+
},
|
|
41
|
+
"dependencies": {
|
|
42
|
+
"@chronicle.app/auth": "0.3.0",
|
|
43
|
+
"axios": "^1.7.9",
|
|
44
|
+
"zod": "^3.24.1"
|
|
45
|
+
},
|
|
46
|
+
"peerDependencies": {
|
|
47
|
+
"@chronicle.app/etl": ">=0.1.0 <1.0.0",
|
|
48
|
+
"@chronicle.app/schema": ">=0.1.0 <1.0.0"
|
|
49
|
+
},
|
|
50
|
+
"engines": {
|
|
51
|
+
"node": ">=22.13.0"
|
|
52
|
+
},
|
|
53
|
+
"exports": {
|
|
54
|
+
".": {
|
|
55
|
+
"types": "./dist/index.d.ts",
|
|
56
|
+
"import": "./dist/index.js"
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
"chronicle": {
|
|
60
|
+
"plugin": true,
|
|
61
|
+
"sources": {
|
|
62
|
+
"pinboard": {
|
|
63
|
+
"strategies": {
|
|
64
|
+
"api": {
|
|
65
|
+
"delivery": "api",
|
|
66
|
+
"recordTypes": [
|
|
67
|
+
"bookmarks"
|
|
68
|
+
],
|
|
69
|
+
"default": true
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|