ticketlens 0.22.1 → 0.23.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/README.md
CHANGED
|
@@ -411,7 +411,7 @@ Every note is scanned before saving — anything shaped like a real secret (API
|
|
|
411
411
|
|
|
412
412
|
**Removing a note:** `ticketlens note delete --id="..." [--ticket=KEY]` removes a note from your local vault. Local only — if it was already pushed to a team, teammates who pulled it keep their copy; deleting it there too is a manager action from the Console (Admin > Recall).
|
|
413
413
|
|
|
414
|
-
**Any MCP-capable AI harness:** `ticketlens mcp` starts a stdio [MCP](https://modelcontextprotocol.io) server exposing `recall_add` and `recall_search` as native tools — any MCP-compatible AI assistant, not just Claude Code, can call them directly instead of constructing a shell command. It's a thin adapter over the exact same code as the CLI commands above — same Pro gate, same secret scan, same local vault, same team sync — nothing is reimplemented. Point your harness's MCP config at it: `{ "command": "ticketlens", "args": ["mcp"] }`.
|
|
414
|
+
**Any MCP-capable AI harness:** `ticketlens mcp` starts a stdio [MCP](https://modelcontextprotocol.io) server exposing `recall_add` and `recall_search` as native tools — any MCP-compatible AI assistant, not just Claude Code, can call them directly instead of constructing a shell command. It's a thin adapter over the exact same code as the CLI commands above — same Pro gate, same secret scan, same local vault, same team sync — nothing is reimplemented. Point your harness's MCP config at it: `{ "command": "ticketlens", "args": ["mcp"] }` — or run `ticketlens mcp install` in a project to write that entry into its `.mcp.json` for you (creates the file if it doesn't exist, merges in if it does — never touches any other entry already there; `--dry-run` to preview first).
|
|
415
415
|
|
|
416
416
|
`note add`'s save confirmation and `recall`'s search results are styled by default in a terminal; add `--plain` to either for bare, pipe-safe output. `recall` always shows each note's file ID (e.g. `[1784135399545-fe01c4.md]`) so you can open it directly (`cat ~/.ticketlens/recall/<PREFIX>/<id>`), or pass `--full` to print the full body content inline instead.
|
|
417
417
|
|
|
@@ -693,6 +693,8 @@ ticketlens recall "retry backoff" # Free-text search across all note
|
|
|
693
693
|
ticketlens recall sync # Retry any notes stuck in the local queue [Team+]
|
|
694
694
|
ticketlens recall settings # Show effective retry-queue settings, fetched live [Team+]
|
|
695
695
|
ticketlens mcp # Start the MCP stdio server (recall_add/recall_search tools) [Pro]
|
|
696
|
+
ticketlens mcp install # Register it into the current project's .mcp.json
|
|
697
|
+
ticketlens mcp install --dry-run # Preview the registration without writing
|
|
696
698
|
|
|
697
699
|
# ── Stats ──────────────────────────────────────────────────────────────────────
|
|
698
700
|
ticketlens stats # Response-time metrics from local history
|
package/bin/ticketlens.mjs
CHANGED
|
@@ -724,6 +724,12 @@ switch (command) {
|
|
|
724
724
|
|
|
725
725
|
case 'mcp': {
|
|
726
726
|
if (cmdArgs.includes('--help') || cmdArgs.includes('-h')) { printMcpHelp(); break; }
|
|
727
|
+
if (cmdArgs[0] === 'install') {
|
|
728
|
+
const { mcpInstall } = await import('../skills/jtb/scripts/lib/mcp-install.mjs');
|
|
729
|
+
const { written, dryRun, reason } = mcpInstall({ dryRun: cmdArgs.includes('--dry-run') });
|
|
730
|
+
if (!dryRun && !written && reason) process.exitCode = 1;
|
|
731
|
+
break;
|
|
732
|
+
}
|
|
727
733
|
// Long-lived stdio server, not a one-shot command — no .then()-exit-code
|
|
728
734
|
// pattern; it exits when the client closes stdin (spec's stdio lifecycle).
|
|
729
735
|
const { runMcpServer } = await import('../skills/jtb/scripts/lib/mcp-server.mjs');
|
package/package.json
CHANGED
package/scripts/postinstall.mjs
CHANGED
|
@@ -102,6 +102,7 @@ try {
|
|
|
102
102
|
} else {
|
|
103
103
|
console.log(' → New here? Run tl to launch the guided setup.');
|
|
104
104
|
}
|
|
105
|
+
console.log(' → Using an MCP-capable AI harness? Run tl mcp install in a project to register Recall as native tools.');
|
|
105
106
|
console.log('');
|
|
106
107
|
} catch {
|
|
107
108
|
// Silent on failure — never breaks the install.
|
package/skills/jtb/SKILL.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<!-- jtb-skill-version: 0.
|
|
1
|
+
<!-- jtb-skill-version: 0.23.0 -->
|
|
2
2
|
---
|
|
3
3
|
name: jtb
|
|
4
4
|
description: Fetch a Jira ticket's full context (description, comments, linked issues, code references) and assemble a structured TicketBrief for implementation planning. Use when user types /jtb, mentions a Jira ticket key, or wants to plan work from a Jira ticket.
|
|
@@ -602,6 +602,13 @@ export function printMcpHelp({ stream = process.stdout } = {}) {
|
|
|
602
602
|
'',
|
|
603
603
|
` ${s.brand('-h')}, ${s.brand('--help')} Show this help`,
|
|
604
604
|
'',
|
|
605
|
+
` ${s.bold('COMMANDS')}`,
|
|
606
|
+
'',
|
|
607
|
+
` ${s.brand('install')} Register "ticketlens mcp" into the current project's .mcp.json`,
|
|
608
|
+
'',
|
|
609
|
+
` ${s.dim('install is ungated — writes the registration for anyone; the tools it')}`,
|
|
610
|
+
` ${s.dim('points at still require Pro at call time, same as note add/recall.')}`,
|
|
611
|
+
'',
|
|
605
612
|
` ${s.bold('HARNESS CONFIG')}`,
|
|
606
613
|
'',
|
|
607
614
|
` ${s.dim('{ "command": "ticketlens", "args": ["mcp"] }')}`,
|
|
@@ -609,6 +616,8 @@ export function printMcpHelp({ stream = process.stdout } = {}) {
|
|
|
609
616
|
` ${s.bold('EXAMPLES')}`,
|
|
610
617
|
'',
|
|
611
618
|
` ${s.dim('$')} ticketlens mcp`,
|
|
619
|
+
` ${s.dim('$')} ticketlens mcp install`,
|
|
620
|
+
` ${s.dim('$')} ticketlens mcp install --dry-run`,
|
|
612
621
|
'',
|
|
613
622
|
];
|
|
614
623
|
stream.write(lines.join('\n') + '\n');
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Registers `ticketlens mcp` into the current project's `.mcp.json` so the
|
|
3
|
+
* user doesn't have to hand-write the JSON. Project-scoped only — Claude
|
|
4
|
+
* Code's MCP config has no global/user-level equivalent (confirmed by
|
|
5
|
+
* inspecting a real ~/.claude.json: no top-level `mcpServers` key, it's
|
|
6
|
+
* nested per-project). Never touches ~/.claude.json directly.
|
|
7
|
+
*
|
|
8
|
+
* Mirrors hooks-setup.mjs's already-proven safe-merge pattern: malformed
|
|
9
|
+
* JSON is left untouched (never overwritten), the write is atomic (temp
|
|
10
|
+
* file + rename — real guarantee on POSIX, still the best available
|
|
11
|
+
* cross-platform approach on Windows), and every other key in the file is
|
|
12
|
+
* preserved exactly. Registration itself is never license-gated — same as
|
|
13
|
+
* `ticketlens mcp`/`note add`/`recall`, which all exist for everyone and
|
|
14
|
+
* gate only at actual invocation.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { existsSync, readFileSync, writeFileSync, renameSync } from 'node:fs';
|
|
18
|
+
import { join } from 'node:path';
|
|
19
|
+
import { DEFAULT_CONFIG_DIR } from './config.mjs';
|
|
20
|
+
import { isLicensed } from './license.mjs';
|
|
21
|
+
import { createStyler } from './ansi.mjs';
|
|
22
|
+
|
|
23
|
+
const ENTRY_NAME = 'ticketlens';
|
|
24
|
+
|
|
25
|
+
function readConfig(configPath) {
|
|
26
|
+
if (!existsSync(configPath)) return { ok: true, config: {} };
|
|
27
|
+
let parsed;
|
|
28
|
+
try {
|
|
29
|
+
parsed = JSON.parse(readFileSync(configPath, 'utf8'));
|
|
30
|
+
} catch {
|
|
31
|
+
return { ok: false, reason: `${configPath} is not valid JSON — left untouched. Fix or remove it, then retry.` };
|
|
32
|
+
}
|
|
33
|
+
// Valid JSON whose top level isn't a plain object (array/null/number/string/
|
|
34
|
+
// boolean) is still valid to parse — `typeof null === 'object'` and arrays
|
|
35
|
+
// are objects too, so both need an explicit check, not just "did it parse."
|
|
36
|
+
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
|
|
37
|
+
return { ok: false, reason: `${configPath}'s top level is not a JSON object — left untouched. Fix or remove it, then retry.` };
|
|
38
|
+
}
|
|
39
|
+
if (parsed.mcpServers !== undefined && (typeof parsed.mcpServers !== 'object' || parsed.mcpServers === null || Array.isArray(parsed.mcpServers))) {
|
|
40
|
+
return { ok: false, reason: `${configPath}'s "mcpServers" key is not an object — left untouched. Fix or remove it, then retry.` };
|
|
41
|
+
}
|
|
42
|
+
return { ok: true, config: parsed };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* @returns {{ written: boolean, dryRun: boolean, path: string, alreadyCorrect?: boolean, reason?: string }}
|
|
47
|
+
*/
|
|
48
|
+
export function mcpInstall({
|
|
49
|
+
cwd = process.cwd(),
|
|
50
|
+
isLicensedFn = isLicensed,
|
|
51
|
+
configDir = DEFAULT_CONFIG_DIR,
|
|
52
|
+
stream = process.stdout,
|
|
53
|
+
dryRun = false,
|
|
54
|
+
} = {}) {
|
|
55
|
+
const configPath = join(cwd, '.mcp.json');
|
|
56
|
+
const s = createStyler({ isTTY: stream.isTTY });
|
|
57
|
+
|
|
58
|
+
const read = readConfig(configPath);
|
|
59
|
+
if (!read.ok) {
|
|
60
|
+
stream.write(` ${s.dim('✗')} ${read.reason}\n`);
|
|
61
|
+
return { written: false, dryRun, path: configPath, reason: read.reason };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const config = read.config;
|
|
65
|
+
config.mcpServers ??= {};
|
|
66
|
+
const desired = { command: ENTRY_NAME, args: ['mcp'] };
|
|
67
|
+
const existing = config.mcpServers[ENTRY_NAME];
|
|
68
|
+
const alreadyCorrect = existing !== undefined && JSON.stringify(existing) === JSON.stringify(desired);
|
|
69
|
+
config.mcpServers[ENTRY_NAME] = desired;
|
|
70
|
+
|
|
71
|
+
const serialized = JSON.stringify(config, null, 2) + '\n';
|
|
72
|
+
|
|
73
|
+
if (dryRun) {
|
|
74
|
+
stream.write(` Would write ${configPath}:\n\n${serialized}\n`);
|
|
75
|
+
return { written: false, dryRun: true, path: configPath, alreadyCorrect };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (alreadyCorrect) {
|
|
79
|
+
stream.write(` ${s.dim('✔')} Already registered in ${configPath}\n`);
|
|
80
|
+
} else {
|
|
81
|
+
const tmpPath = `${configPath}.${process.pid}.tmp`;
|
|
82
|
+
writeFileSync(tmpPath, serialized, 'utf8');
|
|
83
|
+
renameSync(tmpPath, configPath); // atomic on POSIX; best available cross-platform approach on Windows
|
|
84
|
+
stream.write(` ${s.brand('✔')} Registered "ticketlens" in ${configPath}\n`);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (!isLicensedFn('pro', configDir)) {
|
|
88
|
+
stream.write(` ${s.dim('Note: recall_add/recall_search require a Pro license — see')} ${s.cyan('ticketlens license')}${s.dim('.')}\n`);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return { written: !alreadyCorrect, dryRun: false, path: configPath, alreadyCorrect };
|
|
92
|
+
}
|