@longtable/cli 0.1.44 → 0.1.45
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/README.md +6 -5
- package/dist/cli.js +254 -87
- package/dist/longtable-codex-native-hook.js +8 -2
- package/dist/project-session.js +30 -3
- package/dist/search/publisher-access.d.ts +1 -3
- package/dist/search/publisher-access.js +0 -13
- package/dist/search/types.d.ts +0 -6
- package/package.json +7 -7
package/README.md
CHANGED
|
@@ -269,16 +269,17 @@ deduplicates, ranks, and labels results as evidence cards. Some sources work
|
|
|
269
269
|
without keys, some require a contact email, and some need API keys for reliable
|
|
270
270
|
use.
|
|
271
271
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
272
|
+
Scholarly access is configured separately through `longtable access setup`.
|
|
273
|
+
It records readiness for metadata, OA full text, institutional access,
|
|
274
|
+
publisher API/TDM credentials, and manual PDFs without storing secrets.
|
|
275
|
+
Publisher probes cover Elsevier, Springer Nature, Wiley, and Taylor & Francis.
|
|
275
276
|
|
|
276
277
|
Citation support should be checked explicitly. A reference can be useful as
|
|
277
278
|
background while still failing to support the specific claim attached to it.
|
|
278
279
|
|
|
279
280
|
```bash
|
|
280
|
-
longtable
|
|
281
|
-
longtable
|
|
281
|
+
longtable access setup
|
|
282
|
+
longtable access probe --doi "10.1016/example" --publisher elsevier
|
|
282
283
|
longtable search --query "trust calibration measurement" --intent measurement
|
|
283
284
|
longtable search --query "trust calibration measurement" --publisher-access --json
|
|
284
285
|
longtable search --query "trust calibration citation support" --intent citation --record
|
package/dist/cli.js
CHANGED
|
@@ -9,7 +9,7 @@ import { dirname, join, resolve } from "node:path";
|
|
|
9
9
|
import { homedir } from "node:os";
|
|
10
10
|
import { fileURLToPath } from "node:url";
|
|
11
11
|
import { classifyCheckpointTrigger } from "@longtable/checkpoints";
|
|
12
|
-
import { assessSearchSourceCapabilities, buildResearchSearchIntent,
|
|
12
|
+
import { assessSearchSourceCapabilities, buildResearchSearchIntent, parsePublisherTarget, probePublisherAccess, publisherConfigs, runResearchSearch, SEARCH_SOURCES, summarizeConfiguredPublisherAccess } from "./search/index.js";
|
|
13
13
|
import { buildProviderChoices, buildQuickSetupFlow, createPersistedSetupOutput, installRuntimeConfigFromStoredSetup, loadSetupOutput, renderInstallSummary, renderSetupSummary, resolveDefaultRuntimeConfigPath, resolveDefaultSetupPath, saveSetupOutput, saveSetupAndRuntimeConfig, serializeSetupOutput, writeRuntimeConfig } from "@longtable/setup";
|
|
14
14
|
import { buildCodexSkillSpecs, buildCodexThinWrappedPrompt, installCodexSkills, listInstalledCodexSkills, renderQuestionRecordPrompt, removeCodexSkills, resolveCodexSkillsDir, runCodexThinWrapper } from "@longtable/provider-codex";
|
|
15
15
|
import { buildClaudeSkillSpecs, installClaudeSkills, listInstalledClaudeSkills, renderQuestionRecordInput, removeClaudeSkills, resolveClaudeSkillsDir } from "@longtable/provider-claude";
|
|
@@ -109,10 +109,11 @@ function usage() {
|
|
|
109
109
|
" longtable show [--json] [--path <file>]",
|
|
110
110
|
" longtable install [--json] [--path <file>] [--runtime-path <file>]",
|
|
111
111
|
" longtable mcp install [--provider codex|claude|all] [--write] [--checkpoint-ui off|interactive|strong] [--json] [--codex-config <path>] [--claude-settings <path>] [--package <spec>]",
|
|
112
|
+
" longtable access setup [--doi <doi>] [--json]",
|
|
113
|
+
" longtable access status [--json]",
|
|
114
|
+
" longtable access doctor [--doi <doi>] [--publisher auto|elsevier|springer_nature|wiley|taylor_francis|all] [--json]",
|
|
115
|
+
" longtable access probe --doi <doi> [--publisher auto|elsevier|springer_nature|wiley|taylor_francis] [--json]",
|
|
112
116
|
" longtable search --query <text> [--intent literature|theory|measurement|citation|metadata|venue] [--field <text>] [--source all|crossref,arxiv,openalex,semantic_scholar,pubmed,eric,doaj,unpaywall] [--must <term[,term]>] [--exclude <term[,term]>] [--limit <n>] [--allow-partial] [--publisher-access] [--record] [--cwd <path>] [--json]",
|
|
113
|
-
" longtable search setup [--doi <doi>] [--json]",
|
|
114
|
-
" longtable search doctor [--doi <doi>] [--publisher auto|elsevier|springer_nature|wiley|taylor_francis|all] [--json]",
|
|
115
|
-
" longtable search probe --doi <doi> [--publisher auto|elsevier|springer_nature|wiley|taylor_francis] [--json]",
|
|
116
117
|
" longtable sentinel --prompt <text> [--cwd <path>] [--json] [--record]",
|
|
117
118
|
" longtable team --prompt <text> [--role <role[,role]>] [--debate] [--rounds 3|5] [--cwd <path>] [--json]",
|
|
118
119
|
" longtable ask [--prompt <text>] [--print] [--json] [--setup <path>] [--cwd <path>]",
|
|
@@ -154,7 +155,7 @@ function parseArgs(argv) {
|
|
|
154
155
|
const values = {};
|
|
155
156
|
let subcommand = maybeSubcommand;
|
|
156
157
|
const modeCommand = command && VALID_MODES.has(command);
|
|
157
|
-
const directCommand = command && ["init", "setup", "start", "resume", "doctor", "status", "audit", "roles", "show", "install", "mcp", "codex", "claude", "ask", "clarify", "question", "clear-question", "prune-questions", "panel", "decide", "sentinel", "team", "search"].includes(command);
|
|
158
|
+
const directCommand = command && ["init", "setup", "start", "resume", "doctor", "status", "audit", "roles", "show", "install", "mcp", "codex", "claude", "ask", "clarify", "question", "clear-question", "prune-questions", "panel", "decide", "sentinel", "team", "access", "search"].includes(command);
|
|
158
159
|
let startIndex = 1;
|
|
159
160
|
if (modeCommand) {
|
|
160
161
|
subcommand = undefined;
|
|
@@ -163,7 +164,7 @@ function parseArgs(argv) {
|
|
|
163
164
|
else if (command === "codex" || command === "claude" || command === "mcp") {
|
|
164
165
|
startIndex = 2;
|
|
165
166
|
}
|
|
166
|
-
else if (command === "search" && maybeSubcommand && !maybeSubcommand.startsWith("--")) {
|
|
167
|
+
else if ((command === "access" || command === "search") && maybeSubcommand && !maybeSubcommand.startsWith("--")) {
|
|
167
168
|
subcommand = maybeSubcommand;
|
|
168
169
|
startIndex = 2;
|
|
169
170
|
}
|
|
@@ -1789,6 +1790,11 @@ const QUESTION_AUDIT_FIXTURES = [
|
|
|
1789
1790
|
prompt: "Protected decision closure pressure: measurement. User prompt: Implement the plan.",
|
|
1790
1791
|
expectedKinds: ["research_commitment"]
|
|
1791
1792
|
},
|
|
1793
|
+
{
|
|
1794
|
+
id: "scholarly_access_policy",
|
|
1795
|
+
prompt: "메타분석 논문들의 PDF와 full text를 수집해서 원문 기반으로 코딩해줘.",
|
|
1796
|
+
expectedKinds: ["evidence_risk"]
|
|
1797
|
+
},
|
|
1792
1798
|
{
|
|
1793
1799
|
id: "low_stakes_copyedit",
|
|
1794
1800
|
prompt: "문장 끝 공백만 정리해줘.",
|
|
@@ -2347,6 +2353,86 @@ async function recordEvidenceRun(run, workingDirectory) {
|
|
|
2347
2353
|
await syncCurrentWorkspaceView(context);
|
|
2348
2354
|
return evidencePath;
|
|
2349
2355
|
}
|
|
2356
|
+
function nowIso() {
|
|
2357
|
+
return new Date().toISOString();
|
|
2358
|
+
}
|
|
2359
|
+
function uniqueAccessRoutes(routes) {
|
|
2360
|
+
return [...new Set(routes)];
|
|
2361
|
+
}
|
|
2362
|
+
function accessReadinessPath(home = homedir()) {
|
|
2363
|
+
return join(home, ".longtable", "access-readiness.json");
|
|
2364
|
+
}
|
|
2365
|
+
function readAccessReadinessProfile() {
|
|
2366
|
+
const path = accessReadinessPath();
|
|
2367
|
+
if (!existsSync(path)) {
|
|
2368
|
+
return undefined;
|
|
2369
|
+
}
|
|
2370
|
+
try {
|
|
2371
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
2372
|
+
}
|
|
2373
|
+
catch {
|
|
2374
|
+
return undefined;
|
|
2375
|
+
}
|
|
2376
|
+
}
|
|
2377
|
+
function hasPublisherCredentialSignal(records) {
|
|
2378
|
+
return records.some((record) => record.presentEnv.length > 0 || record.credentialStatus !== "missing");
|
|
2379
|
+
}
|
|
2380
|
+
function readinessPublisherRecord(record) {
|
|
2381
|
+
const safeRecord = { ...record };
|
|
2382
|
+
delete safeRecord.evidenceSnippet;
|
|
2383
|
+
return safeRecord;
|
|
2384
|
+
}
|
|
2385
|
+
function inferAccessRoutes(records) {
|
|
2386
|
+
const routes = ["metadata"];
|
|
2387
|
+
if (hasPublisherCredentialSignal(records)) {
|
|
2388
|
+
routes.push("publisher_tdm");
|
|
2389
|
+
}
|
|
2390
|
+
return routes;
|
|
2391
|
+
}
|
|
2392
|
+
function buildAccessReadinessProfile(options) {
|
|
2393
|
+
const publisherRecords = options.publisherRecords ?? summarizeConfiguredPublisherAccess(env);
|
|
2394
|
+
const routes = uniqueAccessRoutes(options.routes?.length ? options.routes : inferAccessRoutes(publisherRecords));
|
|
2395
|
+
const disabled = options.readiness === "disabled";
|
|
2396
|
+
const institutionalMode = options.institutionalAccessMode;
|
|
2397
|
+
const institutionalAccess = routes.includes("institutional")
|
|
2398
|
+
? {
|
|
2399
|
+
available: true,
|
|
2400
|
+
mode: institutionalMode ?? "unknown",
|
|
2401
|
+
verified: false,
|
|
2402
|
+
note: "LongTable records institutional access readiness only. The researcher must complete VPN/proxy/library login directly."
|
|
2403
|
+
}
|
|
2404
|
+
: undefined;
|
|
2405
|
+
return {
|
|
2406
|
+
version: 1,
|
|
2407
|
+
updatedAt: nowIso(),
|
|
2408
|
+
readiness: options.readiness,
|
|
2409
|
+
metadataSources: [...SEARCH_SOURCES],
|
|
2410
|
+
routes: disabled ? [] : routes,
|
|
2411
|
+
...(institutionalAccess ? { institutionalAccess } : {}),
|
|
2412
|
+
publisherTdm: disabled
|
|
2413
|
+
? "deferred"
|
|
2414
|
+
: routes.includes("publisher_tdm")
|
|
2415
|
+
? hasPublisherCredentialSignal(publisherRecords) ? "configured" : "unknown"
|
|
2416
|
+
: "not_configured",
|
|
2417
|
+
oaOnly: !disabled && routes.includes("oa_full_text") && !routes.includes("institutional") && !routes.includes("publisher_tdm"),
|
|
2418
|
+
manualPdfAllowed: !disabled && routes.includes("manual_pdf"),
|
|
2419
|
+
storesSecrets: false,
|
|
2420
|
+
requiresCheckpointBeforeSearch: options.readiness === "deferred",
|
|
2421
|
+
requiresCheckpointBeforeFullText: options.readiness !== "disabled",
|
|
2422
|
+
...(disabled ? {} : {
|
|
2423
|
+
publisherAccess: {
|
|
2424
|
+
contactEmailPresent: Boolean(env.LONGTABLE_CONTACT_EMAIL?.trim()),
|
|
2425
|
+
records: publisherRecords.map(readinessPublisherRecord)
|
|
2426
|
+
}
|
|
2427
|
+
})
|
|
2428
|
+
};
|
|
2429
|
+
}
|
|
2430
|
+
async function saveAccessReadinessProfile(profile) {
|
|
2431
|
+
const profilePath = accessReadinessPath();
|
|
2432
|
+
await mkdir(dirname(profilePath), { recursive: true });
|
|
2433
|
+
await writeJsonFile(profilePath, profile);
|
|
2434
|
+
return profilePath;
|
|
2435
|
+
}
|
|
2350
2436
|
function renderPublisherAccessRecord(record) {
|
|
2351
2437
|
const envSummary = record.missingEnv.length > 0
|
|
2352
2438
|
? `missing ${record.missingEnv.join(", ")}`
|
|
@@ -2381,12 +2467,6 @@ function renderPublisherAccessRecords(title, records, capabilityPath) {
|
|
|
2381
2467
|
}
|
|
2382
2468
|
return lines.join("\n");
|
|
2383
2469
|
}
|
|
2384
|
-
async function saveSearchCapabilityRecords(records) {
|
|
2385
|
-
const snapshotPath = searchCapabilitySnapshotPath();
|
|
2386
|
-
await mkdir(dirname(snapshotPath), { recursive: true });
|
|
2387
|
-
await writeJsonFile(snapshotPath, buildSearchCapabilitySnapshot(records, env));
|
|
2388
|
-
return snapshotPath;
|
|
2389
|
-
}
|
|
2390
2470
|
async function probeAllPublishers(doi) {
|
|
2391
2471
|
const records = [];
|
|
2392
2472
|
for (const publisher of publisherConfigs()) {
|
|
@@ -2398,9 +2478,48 @@ async function probeAllPublishers(doi) {
|
|
|
2398
2478
|
}
|
|
2399
2479
|
return records;
|
|
2400
2480
|
}
|
|
2401
|
-
|
|
2481
|
+
function renderAccessReadinessProfile(profile, profilePath = accessReadinessPath()) {
|
|
2482
|
+
const lines = [
|
|
2483
|
+
"LongTable Scholarly Access Readiness",
|
|
2484
|
+
`- profile: ${profilePath}`,
|
|
2485
|
+
`- readiness: ${profile.readiness}`,
|
|
2486
|
+
`- routes: ${profile.routes.length > 0 ? profile.routes.join(", ") : "none"}`,
|
|
2487
|
+
`- metadata sources: ${profile.metadataSources.join(", ")}`,
|
|
2488
|
+
`- OA-only: ${profile.oaOnly ? "yes" : "no"}`,
|
|
2489
|
+
`- manual PDF allowed: ${profile.manualPdfAllowed ? "yes" : "no"}`,
|
|
2490
|
+
`- publisher/TDM: ${profile.publisherTdm}`,
|
|
2491
|
+
`- stores secrets: ${profile.storesSecrets ? "yes" : "no"}`,
|
|
2492
|
+
`- checkpoint before search: ${profile.requiresCheckpointBeforeSearch ? "yes" : "no"}`,
|
|
2493
|
+
`- checkpoint before full text: ${profile.requiresCheckpointBeforeFullText ? "yes" : "no"}`
|
|
2494
|
+
];
|
|
2495
|
+
if (profile.institutionalAccess) {
|
|
2496
|
+
lines.push(`- institutional access: ${profile.institutionalAccess.mode}; verified: no`);
|
|
2497
|
+
lines.push(` note: ${profile.institutionalAccess.note}`);
|
|
2498
|
+
}
|
|
2499
|
+
if (profile.publisherAccess) {
|
|
2500
|
+
lines.push(`- contact email: ${profile.publisherAccess.contactEmailPresent ? "present" : "missing"}`);
|
|
2501
|
+
lines.push(`- publisher adapters: ${profile.publisherAccess.records.length}`);
|
|
2502
|
+
}
|
|
2503
|
+
return lines.join("\n");
|
|
2504
|
+
}
|
|
2505
|
+
function renderAccessDoctor(profile, records, profilePath) {
|
|
2506
|
+
const metadataCapabilities = assessSearchSourceCapabilities([...SEARCH_SOURCES], env);
|
|
2507
|
+
const lines = [
|
|
2508
|
+
"LongTable Scholarly Access Doctor",
|
|
2509
|
+
`- readiness profile: ${profile ? "present" : "missing"} (${profilePath})`,
|
|
2510
|
+
`- metadata sources: ${metadataCapabilities.map((capability) => `${capability.source}:${capability.enabled ? "available" : "needs_config"}`).join(", ")}`,
|
|
2511
|
+
"- institutional access: LongTable cannot verify VPN/proxy/SSO until the researcher logs in.",
|
|
2512
|
+
"- secrets: LongTable stores env var names and capability status only, never credential values.",
|
|
2513
|
+
renderPublisherAccessRecords("Publisher API/TDM adapters", records)
|
|
2514
|
+
];
|
|
2515
|
+
if (!profile) {
|
|
2516
|
+
lines.push("- next: run `longtable access setup` before PDF collection or full-text extraction.");
|
|
2517
|
+
}
|
|
2518
|
+
return lines.join("\n");
|
|
2519
|
+
}
|
|
2520
|
+
async function runAccessProbe(args) {
|
|
2402
2521
|
if (typeof args.doi !== "string" || !args.doi.trim()) {
|
|
2403
|
-
throw new Error("`longtable
|
|
2522
|
+
throw new Error("`longtable access probe` requires --doi <doi>.");
|
|
2404
2523
|
}
|
|
2405
2524
|
const publisher = parsePublisherTarget(args.publisher);
|
|
2406
2525
|
const record = await probePublisherAccess({
|
|
@@ -2416,7 +2535,7 @@ async function runSearchProbe(args) {
|
|
|
2416
2535
|
}
|
|
2417
2536
|
return [record];
|
|
2418
2537
|
}
|
|
2419
|
-
async function
|
|
2538
|
+
async function collectAccessDoctorRecords(args) {
|
|
2420
2539
|
let records;
|
|
2421
2540
|
if (typeof args.doi === "string" && args.doi.trim()) {
|
|
2422
2541
|
if (args.publisher === "all") {
|
|
@@ -2434,104 +2553,148 @@ async function runSearchDoctor(args) {
|
|
|
2434
2553
|
else {
|
|
2435
2554
|
records = summarizeConfiguredPublisherAccess(env);
|
|
2436
2555
|
}
|
|
2437
|
-
|
|
2438
|
-
|
|
2556
|
+
return records;
|
|
2557
|
+
}
|
|
2558
|
+
async function runAccessDoctor(args) {
|
|
2559
|
+
const records = await collectAccessDoctorRecords(args);
|
|
2560
|
+
const profilePath = accessReadinessPath();
|
|
2561
|
+
const profile = readAccessReadinessProfile();
|
|
2439
2562
|
if (args.json === true) {
|
|
2440
2563
|
console.log(JSON.stringify({
|
|
2441
|
-
|
|
2442
|
-
|
|
2564
|
+
readinessFile: profilePath,
|
|
2565
|
+
readinessFileExists: Boolean(profile),
|
|
2566
|
+
readiness: profile,
|
|
2567
|
+
metadataSources: assessSearchSourceCapabilities([...SEARCH_SOURCES], env),
|
|
2443
2568
|
records
|
|
2444
2569
|
}, null, 2));
|
|
2445
2570
|
}
|
|
2446
2571
|
else {
|
|
2447
|
-
console.log(
|
|
2448
|
-
if (!snapshotExists) {
|
|
2449
|
-
console.log("- saved capabilities: none yet; run `longtable search setup` to record non-secret capability status.");
|
|
2450
|
-
}
|
|
2572
|
+
console.log(renderAccessDoctor(profile, records, profilePath));
|
|
2451
2573
|
}
|
|
2452
2574
|
return records;
|
|
2453
2575
|
}
|
|
2454
|
-
async function
|
|
2455
|
-
const
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
const answer = (await rl.question(prompt)).trim();
|
|
2459
|
-
if (!answer && defaultDoi) {
|
|
2460
|
-
return defaultDoi;
|
|
2461
|
-
}
|
|
2462
|
-
if (!answer || /^skip$/i.test(answer)) {
|
|
2463
|
-
return undefined;
|
|
2576
|
+
async function publisherRecordsForAccessSetup(args, routes) {
|
|
2577
|
+
const defaultDoi = typeof args.doi === "string" ? args.doi : undefined;
|
|
2578
|
+
if (!routes.includes("publisher_tdm")) {
|
|
2579
|
+
return summarizeConfiguredPublisherAccess(env);
|
|
2464
2580
|
}
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
async function runInteractiveSearchSetup(defaultDoi) {
|
|
2468
|
-
const rl = createInterface({ input, output });
|
|
2469
|
-
const records = [];
|
|
2470
|
-
try {
|
|
2471
|
-
console.log("LongTable publisher access setup");
|
|
2472
|
-
console.log("LongTable does not store API keys or TDM tokens. It reads environment variables and records only non-secret capability results.");
|
|
2473
|
-
console.log("");
|
|
2474
|
-
for (const publisher of publisherConfigs()) {
|
|
2475
|
-
console.log(`${publisher.label}`);
|
|
2476
|
-
console.log(` required env: ${publisher.requiredEnv.join(", ")}`);
|
|
2477
|
-
if (publisher.optionalEnv.length > 0) {
|
|
2478
|
-
console.log(` optional env: ${publisher.optionalEnv.join(", ")}`);
|
|
2479
|
-
}
|
|
2480
|
-
console.log(` ${publisher.setupHint}`);
|
|
2481
|
-
const doi = await promptPublisherDoi(rl, publisher.label, defaultDoi);
|
|
2482
|
-
if (doi) {
|
|
2483
|
-
records.push(await probePublisherAccess({
|
|
2484
|
-
doi,
|
|
2485
|
-
publisher: publisher.publisher,
|
|
2486
|
-
env
|
|
2487
|
-
}));
|
|
2488
|
-
}
|
|
2489
|
-
else {
|
|
2490
|
-
const summary = summarizeConfiguredPublisherAccess(env)
|
|
2491
|
-
.find((record) => record.publisher === publisher.publisher);
|
|
2492
|
-
if (summary) {
|
|
2493
|
-
records.push(summary);
|
|
2494
|
-
}
|
|
2495
|
-
}
|
|
2496
|
-
console.log(renderPublisherAccessRecord(records[records.length - 1]));
|
|
2497
|
-
console.log("");
|
|
2498
|
-
}
|
|
2581
|
+
if (defaultDoi) {
|
|
2582
|
+
return probeAllPublishers(defaultDoi);
|
|
2499
2583
|
}
|
|
2500
|
-
|
|
2501
|
-
|
|
2584
|
+
if (input.isTTY && output.isTTY && args.json !== true) {
|
|
2585
|
+
const doi = await promptText("Optional DOI for publisher/TDM probing. Leave blank to record env-var readiness only.", false);
|
|
2586
|
+
return doi
|
|
2587
|
+
? await probeAllPublishers(doi)
|
|
2588
|
+
: summarizeConfiguredPublisherAccess(env);
|
|
2502
2589
|
}
|
|
2503
|
-
return
|
|
2590
|
+
return summarizeConfiguredPublisherAccess(env);
|
|
2504
2591
|
}
|
|
2505
|
-
async function
|
|
2506
|
-
const
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
:
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
2592
|
+
async function runInteractiveAccessSetup(args) {
|
|
2593
|
+
const readiness = await promptChoice("Scholarly Access Readiness\n\nWill this machine/account use scholarly search or full-text access?", [
|
|
2594
|
+
{ id: "configured", label: "Configure now", description: "Record access capability without storing secrets." },
|
|
2595
|
+
{ id: "deferred", label: "Ask later", description: "Defer setup and require an access checkpoint before search or extraction." },
|
|
2596
|
+
{ id: "disabled", label: "Do not use", description: "This project will use metadata/manual notes without scholarly full-text access." }
|
|
2597
|
+
]);
|
|
2598
|
+
if (readiness !== "configured") {
|
|
2599
|
+
return buildAccessReadinessProfile({ readiness, routes: [] });
|
|
2600
|
+
}
|
|
2601
|
+
const routeSelections = await promptMultiChoice("Select every scholarly access route that is available or intended. Secrets are never stored.", [
|
|
2602
|
+
{ id: "metadata", label: "Open metadata", description: "Crossref, OpenAlex, Semantic Scholar, PubMed, ERIC, DOAJ, Unpaywall." },
|
|
2603
|
+
{ id: "oa_full_text", label: "OA full text", description: "Use open-access PDF/full-text when legally available." },
|
|
2604
|
+
{ id: "institutional", label: "Institutional access", description: "VPN, library proxy, or browser SSO handled by the researcher." },
|
|
2605
|
+
{ id: "publisher_tdm", label: "Publisher API/TDM", description: "Use configured publisher API/TDM environment variables." },
|
|
2606
|
+
{ id: "manual_pdf", label: "Manual PDFs", description: "Researcher supplies PDFs; LongTable organizes/probes allowed extraction." },
|
|
2607
|
+
{ id: "unknown", label: "Unknown", description: "Keep access uncertain and require a checkpoint before full-text work." }
|
|
2608
|
+
]);
|
|
2609
|
+
const routes = uniqueAccessRoutes(routeSelections.length > 0 ? routeSelections : ["metadata"]);
|
|
2610
|
+
const institutionalAccessMode = routes.includes("institutional")
|
|
2611
|
+
? await promptChoice("How will institutional access be completed? The researcher handles login/MFA directly.", [
|
|
2612
|
+
{ id: "vpn", label: "VPN", description: "Researcher connects through school or institutional VPN." },
|
|
2613
|
+
{ id: "library_proxy", label: "Library proxy", description: "Researcher uses proxy links or library resolver." },
|
|
2614
|
+
{ id: "browser_sso", label: "Browser SSO", description: "Researcher logs into library/publisher SSO in the browser." },
|
|
2615
|
+
{ id: "unknown", label: "Unknown", description: "The route exists but is not yet specified." }
|
|
2616
|
+
])
|
|
2617
|
+
: undefined;
|
|
2618
|
+
const publisherRecords = await publisherRecordsForAccessSetup(args, routes);
|
|
2619
|
+
return buildAccessReadinessProfile({
|
|
2620
|
+
readiness,
|
|
2621
|
+
routes,
|
|
2622
|
+
institutionalAccessMode,
|
|
2623
|
+
publisherRecords
|
|
2624
|
+
});
|
|
2625
|
+
}
|
|
2626
|
+
async function runAccessSetup(args) {
|
|
2627
|
+
const records = typeof args.doi === "string" && args.doi.trim()
|
|
2628
|
+
? await probeAllPublishers(args.doi)
|
|
2629
|
+
: summarizeConfiguredPublisherAccess(env);
|
|
2630
|
+
const profile = input.isTTY && output.isTTY && args.json !== true
|
|
2631
|
+
? await runInteractiveAccessSetup(args)
|
|
2632
|
+
: buildAccessReadinessProfile({
|
|
2633
|
+
readiness: "configured",
|
|
2634
|
+
routes: inferAccessRoutes(records),
|
|
2635
|
+
publisherRecords: records
|
|
2636
|
+
});
|
|
2637
|
+
const profilePath = await saveAccessReadinessProfile(profile);
|
|
2513
2638
|
if (args.json === true) {
|
|
2514
2639
|
console.log(JSON.stringify({
|
|
2515
|
-
|
|
2516
|
-
|
|
2640
|
+
readinessFile: profilePath,
|
|
2641
|
+
profile
|
|
2517
2642
|
}, null, 2));
|
|
2518
2643
|
return;
|
|
2519
2644
|
}
|
|
2520
|
-
console.log(
|
|
2645
|
+
console.log(renderAccessReadinessProfile(profile, profilePath));
|
|
2521
2646
|
}
|
|
2522
|
-
async function
|
|
2523
|
-
|
|
2524
|
-
|
|
2647
|
+
async function runAccessStatus(args) {
|
|
2648
|
+
const profilePath = accessReadinessPath();
|
|
2649
|
+
const profile = readAccessReadinessProfile();
|
|
2650
|
+
if (args.json === true) {
|
|
2651
|
+
console.log(JSON.stringify({
|
|
2652
|
+
readinessFile: profilePath,
|
|
2653
|
+
readinessFileExists: Boolean(profile),
|
|
2654
|
+
readiness: profile
|
|
2655
|
+
}, null, 2));
|
|
2525
2656
|
return;
|
|
2526
2657
|
}
|
|
2527
|
-
if (
|
|
2528
|
-
|
|
2658
|
+
if (!profile) {
|
|
2659
|
+
console.log([
|
|
2660
|
+
"LongTable Scholarly Access Readiness",
|
|
2661
|
+
`- profile: missing (${profilePath})`,
|
|
2662
|
+
"- next: run `longtable access setup` before PDF collection or full-text extraction."
|
|
2663
|
+
].join("\n"));
|
|
2529
2664
|
return;
|
|
2530
2665
|
}
|
|
2666
|
+
console.log(renderAccessReadinessProfile(profile, profilePath));
|
|
2667
|
+
}
|
|
2668
|
+
async function runAccess(subcommand, args) {
|
|
2531
2669
|
if (subcommand === "setup") {
|
|
2532
|
-
await
|
|
2670
|
+
await runAccessSetup(args);
|
|
2671
|
+
return;
|
|
2672
|
+
}
|
|
2673
|
+
if (subcommand === "status") {
|
|
2674
|
+
await runAccessStatus(args);
|
|
2675
|
+
return;
|
|
2676
|
+
}
|
|
2677
|
+
if (subcommand === "doctor") {
|
|
2678
|
+
await runAccessDoctor(args);
|
|
2533
2679
|
return;
|
|
2534
2680
|
}
|
|
2681
|
+
if (subcommand === "probe") {
|
|
2682
|
+
await runAccessProbe(args);
|
|
2683
|
+
return;
|
|
2684
|
+
}
|
|
2685
|
+
if (!subcommand) {
|
|
2686
|
+
await runAccessStatus(args);
|
|
2687
|
+
return;
|
|
2688
|
+
}
|
|
2689
|
+
throw new Error(`Unknown access subcommand: ${subcommand}`);
|
|
2690
|
+
}
|
|
2691
|
+
function movedSearchAccessCommand(subcommand) {
|
|
2692
|
+
return new Error(`\`longtable search ${subcommand}\` has moved. Use \`longtable access ${subcommand}\`.`);
|
|
2693
|
+
}
|
|
2694
|
+
async function runSearch(subcommand, args) {
|
|
2695
|
+
if (subcommand === "setup" || subcommand === "doctor" || subcommand === "status" || subcommand === "probe") {
|
|
2696
|
+
throw movedSearchAccessCommand(subcommand);
|
|
2697
|
+
}
|
|
2535
2698
|
if (subcommand) {
|
|
2536
2699
|
throw new Error(`Unknown search subcommand: ${subcommand}`);
|
|
2537
2700
|
}
|
|
@@ -3524,6 +3687,10 @@ async function main() {
|
|
|
3524
3687
|
await runMcpSubcommand(subcommand, values);
|
|
3525
3688
|
return;
|
|
3526
3689
|
}
|
|
3690
|
+
if (command === "access") {
|
|
3691
|
+
await runAccess(subcommand, values);
|
|
3692
|
+
return;
|
|
3693
|
+
}
|
|
3527
3694
|
if (command === "search") {
|
|
3528
3695
|
await runSearch(subcommand, values);
|
|
3529
3696
|
return;
|
|
@@ -131,6 +131,11 @@ function looksLikeResearchCommitmentPrompt(prompt) {
|
|
|
131
131
|
/\b(change|revise|update|replace|reframe|modify|alter)\b/i.test(prompt) ||
|
|
132
132
|
/바꾸|변경|수정|교체|전환|재설정/.test(prompt));
|
|
133
133
|
}
|
|
134
|
+
function looksLikeAccessSensitiveResearchAction(prompt) {
|
|
135
|
+
const normalized = prompt.trim();
|
|
136
|
+
return /\b(pdf|full[- ]?text|tdm|publisher api|institutional access|library login|vpn|proxy|subscription|paper collection|source collection|corpus|download)\b/i.test(normalized)
|
|
137
|
+
|| /PDF|원문|전문|기관\s*구독|기관구독|구독|VPN|프록시|도서관|라이브러리|TDM|논문\s*수집|문헌\s*수집|코퍼스|다운로드/.test(normalized);
|
|
138
|
+
}
|
|
134
139
|
function looksLikeQuestionGenerationPrompt(prompt) {
|
|
135
140
|
return /\b(needed questions?|necessary questions?|question generation|clarifying questions?|ask questions?)\b/i.test(prompt)
|
|
136
141
|
|| /필요한\s*질문|질문을\s*(모두|많이|생성)|질문\s*생성|물어봐|질문해/.test(prompt);
|
|
@@ -184,11 +189,12 @@ function shouldSurfaceInterviewContext(prompt) {
|
|
|
184
189
|
return looksLikeExplicitInterviewPrompt(prompt) || looksLikeResearchStateConfirmationPrompt(prompt);
|
|
185
190
|
}
|
|
186
191
|
function shouldCreateRequiredQuestionsForPrompt(prompt) {
|
|
187
|
-
return !looksLikeLongTableProductOrToolingPrompt(prompt) &&
|
|
192
|
+
return !looksLikeLongTableProductOrToolingPrompt(prompt) &&
|
|
193
|
+
(looksLikeResearchCommitmentPrompt(prompt) || looksLikeAccessSensitiveResearchAction(prompt));
|
|
188
194
|
}
|
|
189
195
|
function shouldApplyProtectedDecisionClosure(runtime, prompt) {
|
|
190
196
|
return Boolean(runtime.context.session.protectedDecision) &&
|
|
191
|
-
|
|
197
|
+
looksLikeResearchCommitmentPrompt(prompt) &&
|
|
192
198
|
!looksLikeQuestionGenerationPrompt(prompt) &&
|
|
193
199
|
!looksLikeMultiCommitmentChangePrompt(prompt);
|
|
194
200
|
}
|
package/dist/project-session.js
CHANGED
|
@@ -140,6 +140,15 @@ function renderResearchSpecificationSummary(specification, locale) {
|
|
|
140
140
|
if (specification.methodAnalysis.analysisOptions.length > 0) {
|
|
141
141
|
lines.push(`- ${korean ? "분석 옵션" : "Analysis options"}: ${specification.methodAnalysis.analysisOptions.join("; ")}`);
|
|
142
142
|
}
|
|
143
|
+
if (specification.evidenceAccess.requiredSources?.length) {
|
|
144
|
+
lines.push(`- ${korean ? "필요 근거원" : "Required sources"}: ${specification.evidenceAccess.requiredSources.join("; ")}`);
|
|
145
|
+
}
|
|
146
|
+
if (specification.evidenceAccess.accessRequirements?.length) {
|
|
147
|
+
lines.push(`- ${korean ? "Corpus and Access Plan" : "Corpus and Access Plan"}: ${specification.evidenceAccess.accessRequirements.join("; ")}`);
|
|
148
|
+
}
|
|
149
|
+
if (specification.evidenceAccess.evidenceStandards?.length) {
|
|
150
|
+
lines.push(`- ${korean ? "근거 기준" : "Evidence standards"}: ${specification.evidenceAccess.evidenceStandards.join("; ")}`);
|
|
151
|
+
}
|
|
143
152
|
if (specification.epistemicAlignment.conflictResolutionRule) {
|
|
144
153
|
lines.push(`- ${korean ? "충돌 조정 규칙" : "Conflict rule"}: ${specification.epistemicAlignment.conflictResolutionRule}`);
|
|
145
154
|
}
|
|
@@ -1219,8 +1228,8 @@ function questionPriority(spec) {
|
|
|
1219
1228
|
const requiredWeight = spec.kind === "research_commitment" || spec.required ? 20 : 0;
|
|
1220
1229
|
return (byKey[spec.key] ?? 0) + confidenceWeight + requiredWeight;
|
|
1221
1230
|
}
|
|
1222
|
-
function followUpQuestionOptions(first, second, third,
|
|
1223
|
-
return [first, second, third, ...
|
|
1231
|
+
function followUpQuestionOptions(first, second, third, ...rest) {
|
|
1232
|
+
return [first, second, third, ...rest];
|
|
1224
1233
|
}
|
|
1225
1234
|
export function buildQuestionOpportunitySpecs(prompt, options = {}) {
|
|
1226
1235
|
const normalized = prompt.toLowerCase();
|
|
@@ -1285,8 +1294,26 @@ export function buildQuestionOpportunitySpecs(prompt, options = {}) {
|
|
|
1285
1294
|
/\brandom[- ]?effects\b/i,
|
|
1286
1295
|
/분석\s*계획|분석\s*방법|메타\s*분석|분석\s*(?:모형|모델)|통계\s*(?:모형|모델)|구조\s*방정식|경로\s*모형|조절효과|랜덤\s*효과/
|
|
1287
1296
|
]);
|
|
1297
|
+
const accessCue = includesAny(normalized, [
|
|
1298
|
+
/\b(pdf|full[- ]?text|tdm|publisher api|institutional access|library login|vpn|proxy|subscription|paper collection|source collection|corpus|download)\b/i,
|
|
1299
|
+
/PDF|원문|전문|기관\s*구독|기관구독|구독|VPN|프록시|도서관|라이브러리|TDM|논문\s*수집|문헌\s*수집|코퍼스|다운로드/
|
|
1300
|
+
]);
|
|
1288
1301
|
const decisionFamilyCount = [scopeCue, theoryCue, measurementCodingCue, methodCue, analysisCue]
|
|
1289
1302
|
.filter(Boolean).length;
|
|
1303
|
+
if (accessCue) {
|
|
1304
|
+
push({
|
|
1305
|
+
key: "scholarly_access_policy",
|
|
1306
|
+
kind: "evidence_risk",
|
|
1307
|
+
title: "Scholarly access policy",
|
|
1308
|
+
question: "What scholarly access route should LongTable use before collecting PDFs, full text, or subscription-only evidence?",
|
|
1309
|
+
whyNow: "Full-text access decisions can change the corpus, inclusion bias, reproducibility, and TDM permission boundary.",
|
|
1310
|
+
options: followUpQuestionOptions({ value: "oa_only", label: "OA-only", description: "Use only open-access PDF or full text.", recommended: true }, { value: "institutional_access", label: "Institutional access", description: "Include VPN/proxy/library-login access after the researcher completes login." }, { value: "publisher_tdm", label: "Publisher API/TDM", description: "Use configured publisher API/TDM credentials and record entitlement checks." }, { value: "manual_pdf", label: "Manual PDFs", description: "Use PDFs supplied by the researcher and record provenance." }, { value: "metadata_only", label: "Metadata only", description: "Do not collect full text yet." }),
|
|
1311
|
+
confidence: "high",
|
|
1312
|
+
autoEligible: true,
|
|
1313
|
+
required: true,
|
|
1314
|
+
cues: ["scholarly_access", "full_text", "corpus"]
|
|
1315
|
+
});
|
|
1316
|
+
}
|
|
1290
1317
|
if (decisionActionCue && decisionFamilyCount >= 2) {
|
|
1291
1318
|
push({
|
|
1292
1319
|
key: "research_direction_change_commitment",
|
|
@@ -1619,7 +1646,7 @@ export function buildQuestionOpportunitySpecs(prompt, options = {}) {
|
|
|
1619
1646
|
}
|
|
1620
1647
|
let selected = options.autoOnly === true ? specs.filter((spec) => spec.autoEligible) : specs;
|
|
1621
1648
|
if (options.requiredOnly === true) {
|
|
1622
|
-
selected = selected.filter((spec) => spec.kind === "research_commitment");
|
|
1649
|
+
selected = selected.filter((spec) => spec.kind === "research_commitment" || spec.required);
|
|
1623
1650
|
}
|
|
1624
1651
|
if (normalized.includes("protected decision closure pressure")) {
|
|
1625
1652
|
selected = selected.filter((spec) => spec.key === "protected_decision_closure");
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type CrossrefTdmDiscovery, type EvidenceCard, type Publisher, type PublisherAccessRecord, type PublisherProbeInput, type PublisherProbeTarget, type
|
|
1
|
+
import { type CrossrefTdmDiscovery, type EvidenceCard, type Publisher, type PublisherAccessRecord, type PublisherProbeInput, type PublisherProbeTarget, type SearchFetch } from "./types.js";
|
|
2
2
|
interface PublisherConfig {
|
|
3
3
|
publisher: Publisher;
|
|
4
4
|
label: string;
|
|
@@ -12,8 +12,6 @@ export declare function discoverCrossrefTdm(doi: string, env?: Record<string, st
|
|
|
12
12
|
export declare function publisherConfigs(): PublisherConfig[];
|
|
13
13
|
export declare function probePublisherAccess(input: PublisherProbeInput): Promise<PublisherAccessRecord>;
|
|
14
14
|
export declare function summarizeConfiguredPublisherAccess(env?: Record<string, string | undefined>): PublisherAccessRecord[];
|
|
15
|
-
export declare function buildSearchCapabilitySnapshot(records: PublisherAccessRecord[], env?: Record<string, string | undefined>): SearchCapabilitySnapshot;
|
|
16
|
-
export declare function searchCapabilitySnapshotPath(home?: string): string;
|
|
17
15
|
export declare function enrichCardsWithPublisherAccess(input: {
|
|
18
16
|
cards: EvidenceCard[];
|
|
19
17
|
env?: Record<string, string | undefined>;
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import { join } from "node:path";
|
|
2
|
-
import { homedir } from "node:os";
|
|
3
1
|
import { PUBLISHERS } from "./types.js";
|
|
4
2
|
const PUBLISHER_CONFIGS = {
|
|
5
3
|
elsevier: {
|
|
@@ -505,17 +503,6 @@ export function summarizeConfiguredPublisherAccess(env = process.env) {
|
|
|
505
503
|
});
|
|
506
504
|
});
|
|
507
505
|
}
|
|
508
|
-
export function buildSearchCapabilitySnapshot(records, env = process.env) {
|
|
509
|
-
return {
|
|
510
|
-
version: 1,
|
|
511
|
-
updatedAt: now(),
|
|
512
|
-
contactEmailPresent: Boolean(env.LONGTABLE_CONTACT_EMAIL?.trim()),
|
|
513
|
-
records
|
|
514
|
-
};
|
|
515
|
-
}
|
|
516
|
-
export function searchCapabilitySnapshotPath(home = homedir()) {
|
|
517
|
-
return join(home, ".longtable", "search-capabilities.json");
|
|
518
|
-
}
|
|
519
506
|
function bestAccessStatus(record) {
|
|
520
507
|
if (record.entitlementStatus === "licensed_full_text_available" && record.collectionDepth === "licensed_snippet") {
|
|
521
508
|
return "licensed_full_text_checked";
|
package/dist/search/types.d.ts
CHANGED
|
@@ -173,12 +173,6 @@ export interface PublisherAccessRecord {
|
|
|
173
173
|
evidenceSnippet?: string;
|
|
174
174
|
crossref?: CrossrefTdmDiscovery;
|
|
175
175
|
}
|
|
176
|
-
export interface SearchCapabilitySnapshot {
|
|
177
|
-
version: 1;
|
|
178
|
-
updatedAt: string;
|
|
179
|
-
contactEmailPresent: boolean;
|
|
180
|
-
records: PublisherAccessRecord[];
|
|
181
|
-
}
|
|
182
176
|
export interface PublisherProbeInput {
|
|
183
177
|
doi: string;
|
|
184
178
|
publisher?: PublisherProbeTarget;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@longtable/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.45",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Researcher-facing LongTable CLI",
|
|
6
6
|
"type": "module",
|
|
@@ -29,12 +29,12 @@
|
|
|
29
29
|
},
|
|
30
30
|
"dependencies": {
|
|
31
31
|
"@clack/prompts": "^1.2.0",
|
|
32
|
-
"@longtable/checkpoints": "0.1.
|
|
33
|
-
"@longtable/core": "0.1.
|
|
34
|
-
"@longtable/memory": "0.1.
|
|
35
|
-
"@longtable/provider-claude": "0.1.
|
|
36
|
-
"@longtable/provider-codex": "0.1.
|
|
37
|
-
"@longtable/setup": "0.1.
|
|
32
|
+
"@longtable/checkpoints": "0.1.45",
|
|
33
|
+
"@longtable/core": "0.1.45",
|
|
34
|
+
"@longtable/memory": "0.1.45",
|
|
35
|
+
"@longtable/provider-claude": "0.1.45",
|
|
36
|
+
"@longtable/provider-codex": "0.1.45",
|
|
37
|
+
"@longtable/setup": "0.1.45"
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|
|
40
40
|
"@types/node": "^22.10.1",
|