@insitue/claude-plugin 0.6.3 → 0.7.1
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/.claude-plugin/plugin.json +7 -2
- package/CHANGELOG.md +8 -0
- package/README.md +29 -0
- package/commands/connect.md +34 -0
- package/commands/logout.md +28 -0
- package/dist/{chunk-B3HSTDGI.js → chunk-EDGPZTOV.js} +16 -0
- package/dist/{chunk-IRPBZWNQ.js → chunk-ESW573VH.js} +8 -1
- package/dist/{chunk-KGRPDRYH.js → chunk-ZLNSDVTY.js} +1 -1
- package/dist/cloud/api.js +5 -3
- package/dist/cloud/config.js +3 -1
- package/dist/cloud-cli.js +18 -1
- package/dist/diagnose.js +2 -2
- package/dist/dispatcher.js +5 -2
- package/dist/mcp-server.js +95 -8
- package/dist/setup-cli.js +2 -2
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "insitue",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.1",
|
|
4
4
|
"description": "Drive a Claude Code session from the InSitue browser overlay. Pick an element in your app, claude reads the file and proposes the edit.",
|
|
5
5
|
"mcpServers": {
|
|
6
6
|
"insitue": {
|
|
@@ -11,5 +11,10 @@
|
|
|
11
11
|
],
|
|
12
12
|
"cwd": "${CLAUDE_PROJECT_DIR}"
|
|
13
13
|
}
|
|
14
|
-
}
|
|
14
|
+
},
|
|
15
|
+
"channels": [
|
|
16
|
+
{
|
|
17
|
+
"server": "insitue"
|
|
18
|
+
}
|
|
19
|
+
]
|
|
15
20
|
}
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# @insitue/claude-plugin
|
|
2
2
|
|
|
3
|
+
## 0.7.1
|
|
4
|
+
|
|
5
|
+
- **`/insitue:logout`.** New slash command (+ `logout` MCP tool + `npx @insitue/claude-plugin logout` CLI) that signs you out properly: it revokes this machine's token server-side, then clears the local credentials (`~/.insitue/auth.json`). Revoke is best-effort — your local creds are always cleared even if the network call fails. Pairs with the existing `/insitue:login`.
|
|
6
|
+
|
|
7
|
+
## 0.7.0
|
|
8
|
+
|
|
9
|
+
- **Live picks without blocking the chat (channels, opt-in preview).** The MCP server now also PUSHES each pick as a Claude Code channel event (`notifications/claude/channel`). Start your session with `claude --dangerously-load-development-channels --channels plugin:insitue` and picks wake the idle agent instead of you waiting on a poll — the chat stays free for conversation the rest of the time. Plain `claude` + `/insitue:connect` is unchanged (8s poll fallback). The agent de-dupes by pick id, so the push + poll paths never double-handle a pick. Channels are a Claude Code research-preview feature (CLI-only); the flag is required during the preview.
|
|
10
|
+
|
|
3
11
|
## 0.6.3
|
|
4
12
|
|
|
5
13
|
- **Responsive watch — interject without cancelling.** The `next_pick` long-poll is now 8s (was 25s). While the watch is running you can just type a message: Claude Code queues it and delivers it the moment the poll returns (≤~8s), so you no longer have to cancel the watch to ask a question. The connect instructions now make clear the watch never needs to be cancelled to talk — Claude answers and resumes watching.
|
package/README.md
CHANGED
|
@@ -177,6 +177,35 @@ before writing. After it writes, your dev server's HMR picks up
|
|
|
177
177
|
the change and you see it live in the browser. Pick the next
|
|
178
178
|
thing.
|
|
179
179
|
|
|
180
|
+
### Live picks without blocking the chat (channels, preview)
|
|
181
|
+
|
|
182
|
+
By default the agent polls `next_pick` every ~8 s — the chat is
|
|
183
|
+
technically available between polls, but the polling loop runs
|
|
184
|
+
continuously in the background.
|
|
185
|
+
|
|
186
|
+
As an opt-in upgrade (Claude Code CLI only, research preview),
|
|
187
|
+
you can have picks **pushed** directly into the session so the
|
|
188
|
+
chat stays completely free between picks:
|
|
189
|
+
|
|
190
|
+
```bash
|
|
191
|
+
claude --dangerously-load-development-channels --channels plugin:insitue
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
With this flag, every pick the user sends from the browser wakes
|
|
195
|
+
Claude immediately via a channel notification — no polling needed,
|
|
196
|
+
and the chat is genuinely idle between picks. Claude still buffers
|
|
197
|
+
picks for `next_pick` in parallel, so a stale session or a missing
|
|
198
|
+
channel listener falls back to the normal poll automatically.
|
|
199
|
+
|
|
200
|
+
**Notes:**
|
|
201
|
+
- This is a **Claude Code research-preview** feature. The flag
|
|
202
|
+
`--dangerously-load-development-channels` is required during the
|
|
203
|
+
preview period and may change or be renamed in a future release.
|
|
204
|
+
- It works only with the CLI (`claude`), not Claude Desktop.
|
|
205
|
+
- The standard `/insitue:connect` workflow (plain `claude`, no
|
|
206
|
+
extra flags) continues to work exactly as before via polling —
|
|
207
|
+
channels are purely an opt-in upgrade, not a requirement.
|
|
208
|
+
|
|
180
209
|
---
|
|
181
210
|
|
|
182
211
|
## What gets shipped to claude
|
package/commands/connect.md
CHANGED
|
@@ -25,6 +25,40 @@ that means:
|
|
|
25
25
|
`read_file` tools exposed by this same MCP server
|
|
26
26
|
Either path is fine; pick whichever your runtime has.
|
|
27
27
|
|
|
28
|
+
## Pick delivery modes and idempotency
|
|
29
|
+
|
|
30
|
+
Picks can reach you in one of two ways depending on how `claude`
|
|
31
|
+
was started. **Act on each pick `id` exactly once** regardless of
|
|
32
|
+
which path delivered it — the server always does both, so
|
|
33
|
+
double-delivery is possible and you must de-dupe by `id`.
|
|
34
|
+
|
|
35
|
+
### Push mode (channels — opt-in, Claude Code only)
|
|
36
|
+
|
|
37
|
+
If the user started Claude Code with channel support
|
|
38
|
+
(`claude --dangerously-load-development-channels --channels plugin:insitue`),
|
|
39
|
+
picks are PUSHED into this session as
|
|
40
|
+
`<channel source="insitue">…</channel>` events. Each event
|
|
41
|
+
contains the pick's `id`, `userNote`, `source file:line`,
|
|
42
|
+
`confidence`, and URL — everything you need to act.
|
|
43
|
+
|
|
44
|
+
**In push mode you do NOT need to loop `next_pick`.** Stay idle
|
|
45
|
+
between picks so the user can chat freely. Each pushed pick wakes
|
|
46
|
+
you; handle it exactly like a `next_pick` pick (echo the request,
|
|
47
|
+
read the file, propose the edit, etc.), then go idle again.
|
|
48
|
+
|
|
49
|
+
### Poll mode (plain `claude` — default)
|
|
50
|
+
|
|
51
|
+
If no `<channel>` pick events arrive, you are in poll mode. Loop
|
|
52
|
+
`next_pick` as described in §2 below (default 8 s timeout). This
|
|
53
|
+
is the same behaviour as before channels existed and requires no
|
|
54
|
+
extra flags.
|
|
55
|
+
|
|
56
|
+
### Don't double-handle
|
|
57
|
+
|
|
58
|
+
If the same pick `id` arrives both via a channel event AND via
|
|
59
|
+
`next_pick`, act on it exactly once. Keep a set of handled ids in
|
|
60
|
+
your context and skip any id you've already processed.
|
|
61
|
+
|
|
28
62
|
## Your behaviour
|
|
29
63
|
|
|
30
64
|
1. Call `mcp__insitue__list_recent_picks` once. If there are
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Sign out of InSitue Cloud — revokes this machine's token and clears local credentials.
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# /insitue:logout
|
|
6
|
+
|
|
7
|
+
Signs you out of InSitue Cloud. Revokes this machine's token server-side
|
|
8
|
+
and removes local credentials from `~/.insitue/auth.json`.
|
|
9
|
+
Even if the server-side revoke fails (network issue, expired token), local
|
|
10
|
+
credentials are always cleared.
|
|
11
|
+
|
|
12
|
+
## Your behaviour
|
|
13
|
+
|
|
14
|
+
1. Call `mcp__insitue__logout` (no arguments).
|
|
15
|
+
|
|
16
|
+
2. On `{ status: "ok", revoked: true }`:
|
|
17
|
+
Reply in one line: "Signed out of InSitue."
|
|
18
|
+
|
|
19
|
+
3. On `{ status: "ok", revoked: false }`:
|
|
20
|
+
Reply in one line: "Signed out of InSitue. (Token revoke skipped — no token was stored or the server was unreachable, but local credentials have been cleared.)"
|
|
21
|
+
|
|
22
|
+
4. On any unexpected error: relay the `message` field and suggest running
|
|
23
|
+
`/insitue:logout` again or clearing `~/.insitue/auth.json` manually.
|
|
24
|
+
|
|
25
|
+
## Notes
|
|
26
|
+
|
|
27
|
+
- Logout always clears local credentials, even when the network revoke fails.
|
|
28
|
+
- To sign back in, run `/insitue:login`.
|
|
@@ -40,6 +40,21 @@ function saveAuth(patch) {
|
|
|
40
40
|
mode: 384
|
|
41
41
|
});
|
|
42
42
|
}
|
|
43
|
+
function clearAuth() {
|
|
44
|
+
const p = join(homedir(), ".insitue", "auth.json");
|
|
45
|
+
if (!existsSync(p)) return;
|
|
46
|
+
let existing = {};
|
|
47
|
+
try {
|
|
48
|
+
existing = JSON.parse(readFileSync(p, "utf8"));
|
|
49
|
+
} catch {
|
|
50
|
+
}
|
|
51
|
+
const cleared = {};
|
|
52
|
+
if (existing.host) cleared.host = existing.host;
|
|
53
|
+
writeFileSync(p, JSON.stringify(cleared, null, 2) + "\n", {
|
|
54
|
+
encoding: "utf8",
|
|
55
|
+
mode: 384
|
|
56
|
+
});
|
|
57
|
+
}
|
|
43
58
|
function saveProjectLink(projectDir, projectId) {
|
|
44
59
|
const dir = join(projectDir, ".insitue");
|
|
45
60
|
mkdirSync(dir, { recursive: true });
|
|
@@ -55,5 +70,6 @@ export {
|
|
|
55
70
|
loadProjectId,
|
|
56
71
|
resolveHost,
|
|
57
72
|
saveAuth,
|
|
73
|
+
clearAuth,
|
|
58
74
|
saveProjectLink
|
|
59
75
|
};
|
|
@@ -56,11 +56,18 @@ var releaseIssue = (host, token, id) => call(
|
|
|
56
56
|
"POST",
|
|
57
57
|
`/api/v1/dev/issues/${encodeURIComponent(id)}/release`
|
|
58
58
|
);
|
|
59
|
+
var revokeToken = (host, token) => call(
|
|
60
|
+
host,
|
|
61
|
+
token,
|
|
62
|
+
"POST",
|
|
63
|
+
`/api/v1/dev/token/revoke`
|
|
64
|
+
);
|
|
59
65
|
|
|
60
66
|
export {
|
|
61
67
|
CloudApiError,
|
|
62
68
|
listIssues,
|
|
63
69
|
claimIssue,
|
|
64
70
|
resolveIssue,
|
|
65
|
-
releaseIssue
|
|
71
|
+
releaseIssue,
|
|
72
|
+
revokeToken
|
|
66
73
|
};
|
package/dist/cloud/api.js
CHANGED
|
@@ -3,12 +3,14 @@ import {
|
|
|
3
3
|
claimIssue,
|
|
4
4
|
listIssues,
|
|
5
5
|
releaseIssue,
|
|
6
|
-
resolveIssue
|
|
7
|
-
|
|
6
|
+
resolveIssue,
|
|
7
|
+
revokeToken
|
|
8
|
+
} from "../chunk-ESW573VH.js";
|
|
8
9
|
export {
|
|
9
10
|
CloudApiError,
|
|
10
11
|
claimIssue,
|
|
11
12
|
listIssues,
|
|
12
13
|
releaseIssue,
|
|
13
|
-
resolveIssue
|
|
14
|
+
resolveIssue,
|
|
15
|
+
revokeToken
|
|
14
16
|
};
|
package/dist/cloud/config.js
CHANGED
package/dist/cloud-cli.js
CHANGED
|
@@ -2,12 +2,16 @@ import {
|
|
|
2
2
|
resolveProjectDir
|
|
3
3
|
} from "./chunk-UNMH2DN4.js";
|
|
4
4
|
import {
|
|
5
|
+
revokeToken
|
|
6
|
+
} from "./chunk-ESW573VH.js";
|
|
7
|
+
import {
|
|
8
|
+
clearAuth,
|
|
5
9
|
loadAuth,
|
|
6
10
|
loadProjectId,
|
|
7
11
|
resolveHost,
|
|
8
12
|
saveAuth,
|
|
9
13
|
saveProjectLink
|
|
10
|
-
} from "./chunk-
|
|
14
|
+
} from "./chunk-EDGPZTOV.js";
|
|
11
15
|
import {
|
|
12
16
|
detectGitRemote,
|
|
13
17
|
pickProjectIdForRepo,
|
|
@@ -182,6 +186,18 @@ Run: npx @insitue/claude-plugin link <projectId>
|
|
|
182
186
|
}
|
|
183
187
|
return 1;
|
|
184
188
|
}
|
|
189
|
+
async function cmdLogout() {
|
|
190
|
+
const auth = loadAuth();
|
|
191
|
+
if (auth.token) {
|
|
192
|
+
try {
|
|
193
|
+
await revokeToken(resolveHost(auth), auth.token);
|
|
194
|
+
} catch {
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
clearAuth();
|
|
198
|
+
process.stdout.write("Signed out of InSitue.\n");
|
|
199
|
+
return 0;
|
|
200
|
+
}
|
|
185
201
|
function cmdWhoami() {
|
|
186
202
|
const auth = loadAuth();
|
|
187
203
|
const projectDir = resolveProjectDir();
|
|
@@ -200,5 +216,6 @@ Project: ${projectId ?? "(not linked)"}
|
|
|
200
216
|
export {
|
|
201
217
|
cmdLink,
|
|
202
218
|
cmdLogin,
|
|
219
|
+
cmdLogout,
|
|
203
220
|
cmdWhoami
|
|
204
221
|
};
|
package/dist/diagnose.js
CHANGED
package/dist/dispatcher.js
CHANGED
|
@@ -2,12 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
// src/dispatcher.ts
|
|
4
4
|
var CLI_SUBCOMMANDS = /* @__PURE__ */ new Set(["setup", "diagnose", "help", "--help", "-h"]);
|
|
5
|
-
var CLOUD_SUBCOMMANDS = /* @__PURE__ */ new Set(["login", "link", "whoami"]);
|
|
5
|
+
var CLOUD_SUBCOMMANDS = /* @__PURE__ */ new Set(["login", "link", "whoami", "logout"]);
|
|
6
6
|
var first = process.argv[2];
|
|
7
7
|
if (first && CLI_SUBCOMMANDS.has(first)) {
|
|
8
8
|
await import("./setup-cli.js");
|
|
9
9
|
} else if (first && CLOUD_SUBCOMMANDS.has(first)) {
|
|
10
|
-
const { cmdLogin, cmdLink, cmdWhoami } = await import("./cloud-cli.js");
|
|
10
|
+
const { cmdLogin, cmdLink, cmdWhoami, cmdLogout } = await import("./cloud-cli.js");
|
|
11
11
|
const rest = process.argv.slice(3);
|
|
12
12
|
let code;
|
|
13
13
|
switch (first) {
|
|
@@ -20,6 +20,9 @@ if (first && CLI_SUBCOMMANDS.has(first)) {
|
|
|
20
20
|
case "whoami":
|
|
21
21
|
code = cmdWhoami();
|
|
22
22
|
break;
|
|
23
|
+
case "logout":
|
|
24
|
+
code = await cmdLogout();
|
|
25
|
+
break;
|
|
23
26
|
default:
|
|
24
27
|
code = 0;
|
|
25
28
|
}
|
package/dist/mcp-server.js
CHANGED
|
@@ -5,21 +5,23 @@ import {
|
|
|
5
5
|
} from "./chunk-UNMH2DN4.js";
|
|
6
6
|
import {
|
|
7
7
|
diagnose
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-ZLNSDVTY.js";
|
|
9
9
|
import {
|
|
10
10
|
CloudApiError,
|
|
11
11
|
claimIssue,
|
|
12
12
|
listIssues,
|
|
13
13
|
releaseIssue,
|
|
14
|
-
resolveIssue
|
|
15
|
-
|
|
14
|
+
resolveIssue,
|
|
15
|
+
revokeToken
|
|
16
|
+
} from "./chunk-ESW573VH.js";
|
|
16
17
|
import {
|
|
18
|
+
clearAuth,
|
|
17
19
|
loadAuth,
|
|
18
20
|
loadProjectId,
|
|
19
21
|
resolveHost,
|
|
20
22
|
saveAuth,
|
|
21
23
|
saveProjectLink
|
|
22
|
-
} from "./chunk-
|
|
24
|
+
} from "./chunk-EDGPZTOV.js";
|
|
23
25
|
import {
|
|
24
26
|
detectGitRemote,
|
|
25
27
|
pickProjectIdForRepo,
|
|
@@ -501,6 +503,25 @@ function connectToCompanion(s) {
|
|
|
501
503
|
`[insitue] \u{1F4E5} pick received \u2014 "${note}" @ ${where}
|
|
502
504
|
`
|
|
503
505
|
);
|
|
506
|
+
try {
|
|
507
|
+
const channelContent = `InSitue pick ready (id: ${summary.id})
|
|
508
|
+
Note: ${summary.userNote ?? "(no description)"}
|
|
509
|
+
Source: ${summary.source ? `${summary.source.file}:${summary.source.line ?? "?"}` : "(unknown)"}
|
|
510
|
+
Confidence: ${summary.confidence}
|
|
511
|
+
` + (summary.url ? `URL: ${summary.url}
|
|
512
|
+
` : "");
|
|
513
|
+
void server.server.notification(
|
|
514
|
+
{
|
|
515
|
+
method: "notifications/claude/channel",
|
|
516
|
+
params: {
|
|
517
|
+
content: channelContent,
|
|
518
|
+
meta: { pick_id: summary.id }
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
).catch(() => {
|
|
522
|
+
});
|
|
523
|
+
} catch {
|
|
524
|
+
}
|
|
504
525
|
} catch (err) {
|
|
505
526
|
process.stderr.write(
|
|
506
527
|
`[insitue-mcp] dropped malformed pick: ${err.message}
|
|
@@ -590,10 +611,21 @@ function endSession() {
|
|
|
590
611
|
session = null;
|
|
591
612
|
return { closedWs, killedCompanion, removedSessionFile };
|
|
592
613
|
}
|
|
593
|
-
var server = new McpServer(
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
614
|
+
var server = new McpServer(
|
|
615
|
+
{
|
|
616
|
+
name: "insitue",
|
|
617
|
+
version: readPackageVersion()
|
|
618
|
+
},
|
|
619
|
+
{
|
|
620
|
+
capabilities: {
|
|
621
|
+
// Declare the Claude Code "channel" experimental capability so the
|
|
622
|
+
// MCP server is permitted to emit notifications/claude/channel events.
|
|
623
|
+
// Claude Code checks for this key during its research-preview channel
|
|
624
|
+
// handshake; without it the push notification is silently dropped.
|
|
625
|
+
experimental: { "claude/channel": {} }
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
);
|
|
597
629
|
server.registerTool(
|
|
598
630
|
"next_pick",
|
|
599
631
|
{
|
|
@@ -898,6 +930,37 @@ server.registerTool(
|
|
|
898
930
|
}
|
|
899
931
|
}
|
|
900
932
|
);
|
|
933
|
+
server.registerTool(
|
|
934
|
+
"logout",
|
|
935
|
+
{
|
|
936
|
+
description: "Sign out of InSitue Cloud: best-effort revoke this machine's token server-side, then clear local credentials. Logout always clears local creds even if the revoke call fails (network error, 4xx, etc.).",
|
|
937
|
+
inputSchema: {}
|
|
938
|
+
},
|
|
939
|
+
async () => {
|
|
940
|
+
const cfg = loadAuth();
|
|
941
|
+
let revoked = false;
|
|
942
|
+
if (cfg.token) {
|
|
943
|
+
try {
|
|
944
|
+
await revokeToken(resolveHost(cfg), cfg.token);
|
|
945
|
+
revoked = true;
|
|
946
|
+
} catch {
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
clearAuth();
|
|
950
|
+
return {
|
|
951
|
+
content: [
|
|
952
|
+
{
|
|
953
|
+
type: "text",
|
|
954
|
+
text: JSON.stringify({
|
|
955
|
+
status: "ok",
|
|
956
|
+
revoked,
|
|
957
|
+
message: "Signed out of InSitue."
|
|
958
|
+
})
|
|
959
|
+
}
|
|
960
|
+
]
|
|
961
|
+
};
|
|
962
|
+
}
|
|
963
|
+
);
|
|
901
964
|
function cloudSetup() {
|
|
902
965
|
const auth = loadAuth();
|
|
903
966
|
if (!auth.token) {
|
|
@@ -1132,6 +1195,30 @@ Show the user the returned URL and userCode, then call
|
|
|
1132
1195
|
\`mcp__insitue__complete_authentication\` once they approve in the browser.
|
|
1133
1196
|
Confirm the result with "Signed in as <login>" and linked project if any.`;
|
|
1134
1197
|
}
|
|
1198
|
+
server.registerPrompt(
|
|
1199
|
+
"logout",
|
|
1200
|
+
{
|
|
1201
|
+
title: "Sign out of InSitue",
|
|
1202
|
+
description: "Sign out of InSitue Cloud \u2014 revokes this machine's token and clears local credentials."
|
|
1203
|
+
},
|
|
1204
|
+
() => ({
|
|
1205
|
+
messages: [
|
|
1206
|
+
{
|
|
1207
|
+
role: "user",
|
|
1208
|
+
content: {
|
|
1209
|
+
type: "text",
|
|
1210
|
+
text: readPkgFile("commands/logout.md") ?? logoutInstructions()
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
]
|
|
1214
|
+
})
|
|
1215
|
+
);
|
|
1216
|
+
function logoutInstructions() {
|
|
1217
|
+
return `# Sign out of InSitue
|
|
1218
|
+
|
|
1219
|
+
Call \`mcp__insitue__logout\` (no arguments).
|
|
1220
|
+
Confirm the result in one line, e.g. "Signed out of InSitue." or relay any error message.`;
|
|
1221
|
+
}
|
|
1135
1222
|
function readPkgFile(rel) {
|
|
1136
1223
|
const here = dirname3(fileURLToPath2(import.meta.url));
|
|
1137
1224
|
for (const base of [join3(here, ".."), here]) {
|
package/dist/setup-cli.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@insitue/claude-plugin",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.1",
|
|
4
4
|
"description": "Drive Claude (Code AND Desktop) from the InSitue browser overlay — pick an element in your app, claude reads the file and proposes the edit.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"insitue",
|