@pranjalmandavkar/opencode-notifier 1.0.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 +149 -0
- package/bun.lock +56 -0
- package/dist/index.js +5196 -0
- package/package.json +40 -0
- package/sounds/complete.wav +0 -0
- package/sounds/error.wav +0 -0
- package/sounds/permission.wav +0 -0
- package/sounds/question.wav +0 -0
- package/src/config.ts +157 -0
- package/src/index.ts +69 -0
- package/src/notify.ts +62 -0
- package/src/sound.ts +126 -0
- package/tsconfig.json +29 -0
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@pranjalmandavkar/opencode-notifier",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Opencode Plugin that sends system notification when permission is needed, generation is complete or error occurs in session",
|
|
5
|
+
"author": "MandavkarPranjal",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"main": "dist/index.js",
|
|
9
|
+
"types": "dist/index.d.ts",
|
|
10
|
+
"repository": {
|
|
11
|
+
"type": "git",
|
|
12
|
+
"url": "git+https://github.com/mandavkarpranjal/opencode-notifier.git"
|
|
13
|
+
},
|
|
14
|
+
"homepage": "https://github.com/mandavkarpranjal/opencode-notifier#readme",
|
|
15
|
+
"keywords": [
|
|
16
|
+
"opencode",
|
|
17
|
+
"opencode-plugin",
|
|
18
|
+
"notification",
|
|
19
|
+
"alerts"
|
|
20
|
+
],
|
|
21
|
+
"scripts": {
|
|
22
|
+
"build": "bun build src/index.ts --outdir dist --target node",
|
|
23
|
+
"typecheck": "tsc --noEmit",
|
|
24
|
+
"release:patch": "bun run build && npm version patch && npm publish --access public",
|
|
25
|
+
"release:minor": "bun run build && npm version minor && npm publish --access public",
|
|
26
|
+
"release:major": "bun run build && npm version major && npm publish --access public"
|
|
27
|
+
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"@opencode-ai/plugin": "^1.1.25",
|
|
30
|
+
"@types/bun": "latest",
|
|
31
|
+
"@types/node": "^25.0.9",
|
|
32
|
+
"@types/node-notifier": "^8.0.5"
|
|
33
|
+
},
|
|
34
|
+
"peerDependencies": {
|
|
35
|
+
"@opencode-ai/plugin": "^1.1.25"
|
|
36
|
+
},
|
|
37
|
+
"dependencies": {
|
|
38
|
+
"node-notifier": "^10.0.1"
|
|
39
|
+
}
|
|
40
|
+
}
|
|
Binary file
|
package/sounds/error.wav
ADDED
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { readFileSync, existsSync } from "fs"
|
|
2
|
+
import { join } from "path"
|
|
3
|
+
import { homedir } from "os"
|
|
4
|
+
|
|
5
|
+
export type EventType = "permission" | "complete" | "error" | "question"
|
|
6
|
+
|
|
7
|
+
export interface EventConfig {
|
|
8
|
+
sound: boolean
|
|
9
|
+
notification: boolean
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface NotifierConfig {
|
|
13
|
+
sound: boolean
|
|
14
|
+
notification: boolean
|
|
15
|
+
timeout: number
|
|
16
|
+
showProjectName: boolean
|
|
17
|
+
events: {
|
|
18
|
+
permission: EventConfig
|
|
19
|
+
complete: EventConfig
|
|
20
|
+
error: EventConfig
|
|
21
|
+
question: EventConfig
|
|
22
|
+
}
|
|
23
|
+
messages: {
|
|
24
|
+
permission: string
|
|
25
|
+
complete: string
|
|
26
|
+
error: string
|
|
27
|
+
question: string
|
|
28
|
+
}
|
|
29
|
+
sounds: {
|
|
30
|
+
permission: string | null
|
|
31
|
+
complete: string | null
|
|
32
|
+
error: string | null
|
|
33
|
+
question: string | null
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const DEFAULT_EVENT_CONFIG: EventConfig = {
|
|
38
|
+
sound: true,
|
|
39
|
+
notification: true,
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const DEFAULT_CONFIG: NotifierConfig = {
|
|
43
|
+
sound: true,
|
|
44
|
+
notification: true,
|
|
45
|
+
timeout: 5,
|
|
46
|
+
showProjectName: true,
|
|
47
|
+
events: {
|
|
48
|
+
permission: { ...DEFAULT_EVENT_CONFIG },
|
|
49
|
+
complete: { ...DEFAULT_EVENT_CONFIG },
|
|
50
|
+
error: { ...DEFAULT_EVENT_CONFIG },
|
|
51
|
+
question: { ...DEFAULT_EVENT_CONFIG },
|
|
52
|
+
},
|
|
53
|
+
messages: {
|
|
54
|
+
permission: "Session needs permission",
|
|
55
|
+
complete: "Session has finished",
|
|
56
|
+
error: "Session encountered an error",
|
|
57
|
+
question: "Session has a question",
|
|
58
|
+
},
|
|
59
|
+
sounds: {
|
|
60
|
+
permission: null,
|
|
61
|
+
complete: null,
|
|
62
|
+
error: null,
|
|
63
|
+
question: null
|
|
64
|
+
},
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function getConfigPath(): string {
|
|
68
|
+
return join(homedir(), ".config", "opencode", "opencode-notifier.json")
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function parseEventConfig(
|
|
72
|
+
userEvent: boolean | { sound?: boolean; notification?: boolean } | undefined,
|
|
73
|
+
defaultConfig: EventConfig
|
|
74
|
+
): EventConfig {
|
|
75
|
+
if (userEvent === undefined) {
|
|
76
|
+
return defaultConfig
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (typeof userEvent === "boolean") {
|
|
80
|
+
return {
|
|
81
|
+
sound: userEvent,
|
|
82
|
+
notification: userEvent,
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return {
|
|
87
|
+
sound: userEvent.sound ?? defaultConfig.sound,
|
|
88
|
+
notification: userEvent.notification ?? defaultConfig.notification,
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function loadConfig(): NotifierConfig {
|
|
93
|
+
const configPath = getConfigPath()
|
|
94
|
+
|
|
95
|
+
if (!existsSync(configPath)) {
|
|
96
|
+
return DEFAULT_CONFIG
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
try {
|
|
100
|
+
const fileContent = readFileSync(configPath, "utf-8")
|
|
101
|
+
const userConfig = JSON.parse(fileContent)
|
|
102
|
+
|
|
103
|
+
const globalSound = userConfig.sound ?? DEFAULT_CONFIG.sound
|
|
104
|
+
const globalNotification = userConfig.notification ?? DEFAULT_CONFIG.notification
|
|
105
|
+
|
|
106
|
+
const defaultWithGlobal: EventConfig = {
|
|
107
|
+
sound: globalSound,
|
|
108
|
+
notification: globalNotification,
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
sound: globalSound,
|
|
113
|
+
notification: globalNotification,
|
|
114
|
+
timeout:
|
|
115
|
+
typeof userConfig.timeout === "number" && userConfig.timeout > 0
|
|
116
|
+
? userConfig.timeout
|
|
117
|
+
: DEFAULT_CONFIG.timeout,
|
|
118
|
+
showProjectName: userConfig.showProjectName ?? DEFAULT_CONFIG.showProjectName,
|
|
119
|
+
events: {
|
|
120
|
+
permission: parseEventConfig(userConfig.events?.permission ?? userConfig.permission, defaultWithGlobal),
|
|
121
|
+
complete: parseEventConfig(userConfig.events?.complete ?? userConfig.complete, defaultWithGlobal),
|
|
122
|
+
error: parseEventConfig(userConfig.events?.error ?? userConfig.error, defaultWithGlobal),
|
|
123
|
+
question: parseEventConfig(userConfig.events?.question ?? userConfig.question, defaultWithGlobal),
|
|
124
|
+
},
|
|
125
|
+
messages: {
|
|
126
|
+
permission: userConfig.messages?.permission ?? DEFAULT_CONFIG.messages.permission,
|
|
127
|
+
complete: userConfig.messages?.complete ?? DEFAULT_CONFIG.messages.complete,
|
|
128
|
+
error: userConfig.messages?.error ?? DEFAULT_CONFIG.messages.error,
|
|
129
|
+
question: userConfig.messages?.question ?? DEFAULT_CONFIG.messages.question,
|
|
130
|
+
},
|
|
131
|
+
sounds: {
|
|
132
|
+
permission: userConfig.sounds?.permission ?? DEFAULT_CONFIG.sounds.permission,
|
|
133
|
+
complete: userConfig.sounds?.complete ?? DEFAULT_CONFIG.sounds.complete,
|
|
134
|
+
error: userConfig.sounds?.error ?? DEFAULT_CONFIG.sounds.error,
|
|
135
|
+
question: userConfig.sounds?.question ?? DEFAULT_CONFIG.sounds.question,
|
|
136
|
+
},
|
|
137
|
+
}
|
|
138
|
+
} catch {
|
|
139
|
+
return DEFAULT_CONFIG
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function isEventNotificationEnabled(config: NotifierConfig, event: EventType): boolean {
|
|
144
|
+
return config.events[event].notification
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function getMessage(config: NotifierConfig, event: EventType): string {
|
|
148
|
+
return config.messages[event]
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export function isEventSoundEnabled(config: NotifierConfig, event: EventType): boolean {
|
|
152
|
+
return config.events[event].sound
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export function getSound(config: NotifierConfig, event: EventType): string | null {
|
|
156
|
+
return config.sounds[event]
|
|
157
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { Plugin } from "@opencode-ai/plugin"
|
|
2
|
+
import { basename } from "path"
|
|
3
|
+
import { loadConfig, isEventNotificationEnabled, getMessage, isEventSoundEnabled, getSound } from "./config"
|
|
4
|
+
import type { EventType, NotifierConfig } from "./config"
|
|
5
|
+
import { sendNotification } from "./notify"
|
|
6
|
+
import { playSound } from "./sound"
|
|
7
|
+
|
|
8
|
+
function getNotificationTitle(config: NotifierConfig, projectName: string | null): string {
|
|
9
|
+
if (config.showProjectName && projectName) {
|
|
10
|
+
return `OpenCode (${projectName})`
|
|
11
|
+
}
|
|
12
|
+
return "OpenCode"
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async function handleEvent(
|
|
16
|
+
config: NotifierConfig,
|
|
17
|
+
eventType: EventType,
|
|
18
|
+
projectName: string | null
|
|
19
|
+
): Promise<void> {
|
|
20
|
+
const promises: Promise<void>[] = []
|
|
21
|
+
|
|
22
|
+
if (isEventNotificationEnabled(config, eventType)) {
|
|
23
|
+
const title = getNotificationTitle(config, projectName)
|
|
24
|
+
const message = getMessage(config, eventType)
|
|
25
|
+
promises.push(sendNotification(title, message, config.timeout))
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (isEventSoundEnabled(config, eventType)) {
|
|
29
|
+
const customSound = getSound(config, eventType)
|
|
30
|
+
promises.push(playSound(eventType, customSound))
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
await Promise.allSettled(promises)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export const NotifierPlugin: Plugin = async ({ project, client, $, directory, worktree }) => {
|
|
37
|
+
const config = loadConfig()
|
|
38
|
+
const projectName = directory ? basename(directory) : null
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
event: async ({ event }) => {
|
|
42
|
+
if (event.type === "permission.updated") {
|
|
43
|
+
await handleEvent(config, "permission", projectName)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if ((event as any).type === "permission.asked") {
|
|
47
|
+
await handleEvent(config, "permission", projectName)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (event.type === "session.idle") {
|
|
51
|
+
await handleEvent(config, "complete", projectName)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (event.type === "session.error") {
|
|
55
|
+
await handleEvent(config, "error", projectName)
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
"permission.ask": async () => {
|
|
59
|
+
await handleEvent(config, "permission", projectName)
|
|
60
|
+
},
|
|
61
|
+
"tool.execute.before": async (input, output) => {
|
|
62
|
+
if (input.tool === "question") {
|
|
63
|
+
await handleEvent(config, "question", projectName)
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export default NotifierPlugin
|
package/src/notify.ts
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import os from "os"
|
|
2
|
+
import { exec } from "child_process"
|
|
3
|
+
import notifier from "node-notifier"
|
|
4
|
+
|
|
5
|
+
const DEBOUNCE_MS = 1000
|
|
6
|
+
|
|
7
|
+
const platform = os.type()
|
|
8
|
+
|
|
9
|
+
let platformNotifier: any
|
|
10
|
+
|
|
11
|
+
if (platform === "Linux" || platform.match(/BSD$/)) {
|
|
12
|
+
const { NotifySend } = notifier
|
|
13
|
+
platformNotifier = new NotifySend({ withFallback: false })
|
|
14
|
+
} else if (platform === "Windows_NT") {
|
|
15
|
+
const { WindowsToaster } = notifier
|
|
16
|
+
platformNotifier = new WindowsToaster({ withFallback: false })
|
|
17
|
+
} else if (platform !== "Darwin") {
|
|
18
|
+
platformNotifier = notifier
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const lastNotificationTime: Record<string, number> = {}
|
|
22
|
+
|
|
23
|
+
export async function sendNotification(
|
|
24
|
+
title: string,
|
|
25
|
+
message: string,
|
|
26
|
+
timeout: number
|
|
27
|
+
): Promise<void> {
|
|
28
|
+
const now = Date.now()
|
|
29
|
+
if (lastNotificationTime[message] && now - lastNotificationTime[message] < DEBOUNCE_MS) {
|
|
30
|
+
return
|
|
31
|
+
}
|
|
32
|
+
lastNotificationTime[message] = now
|
|
33
|
+
|
|
34
|
+
if (platform === "Darwin") {
|
|
35
|
+
return new Promise((resolve) => {
|
|
36
|
+
const escapedMessage = message.replace(/"/g, '\\"')
|
|
37
|
+
const escapedTitle = title.replace(/"/g, '\\"')
|
|
38
|
+
exec(
|
|
39
|
+
`osascript -e 'display notification "${escapedMessage}" with title "${escapedTitle}"'`,
|
|
40
|
+
() => {
|
|
41
|
+
resolve()
|
|
42
|
+
}
|
|
43
|
+
)
|
|
44
|
+
})
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return new Promise((resolve) => {
|
|
48
|
+
const notificationOptions: any = {
|
|
49
|
+
title: title,
|
|
50
|
+
message: message,
|
|
51
|
+
timeout: timeout,
|
|
52
|
+
icon: undefined,
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
platformNotifier.notify(
|
|
56
|
+
notificationOptions,
|
|
57
|
+
() => {
|
|
58
|
+
resolve()
|
|
59
|
+
}
|
|
60
|
+
)
|
|
61
|
+
})
|
|
62
|
+
}
|
package/src/sound.ts
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { platform } from "os"
|
|
2
|
+
import { join, dirname } from "path"
|
|
3
|
+
import { fileURLToPath } from "url"
|
|
4
|
+
import { existsSync } from "fs"
|
|
5
|
+
import { spawn } from "child_process"
|
|
6
|
+
import type { EventType } from "./config"
|
|
7
|
+
|
|
8
|
+
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
9
|
+
const DEBOUNCE_MS = 1000
|
|
10
|
+
|
|
11
|
+
const lastSoundTime: Record<string, number> = {}
|
|
12
|
+
|
|
13
|
+
function getBundledSoundPath(event: EventType): string {
|
|
14
|
+
const soundFilename = `${event}.wav`
|
|
15
|
+
|
|
16
|
+
const possiblePaths = [
|
|
17
|
+
join(__dirname, "..", "sounds", soundFilename),
|
|
18
|
+
join(__dirname, "sounds", soundFilename),
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
for (const path of possiblePaths) {
|
|
22
|
+
if (existsSync(path)) {
|
|
23
|
+
return path
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return join(__dirname, "..", "sounds", soundFilename)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function getSoundFilePath(event: EventType, customPath: string | null): string | null {
|
|
31
|
+
if (customPath && existsSync(customPath)) {
|
|
32
|
+
return customPath
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const bundledPath = getBundledSoundPath(event)
|
|
36
|
+
if (existsSync(bundledPath)) {
|
|
37
|
+
return bundledPath
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return null
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function runCommand(command: string, args: string[]): Promise<void> {
|
|
44
|
+
return new Promise((resolve, reject) => {
|
|
45
|
+
const proc = spawn(command, args, {
|
|
46
|
+
stdio: "ignore",
|
|
47
|
+
detached: false,
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
proc.on("error", (err) => {
|
|
51
|
+
reject(err)
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
proc.on("close", (code) => {
|
|
55
|
+
if (code === 0) {
|
|
56
|
+
resolve()
|
|
57
|
+
} else {
|
|
58
|
+
reject(new Error(`Command exited with code ${code}`))
|
|
59
|
+
}
|
|
60
|
+
})
|
|
61
|
+
})
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function playOnLinux(soundPath: string): Promise<void> {
|
|
65
|
+
const players = [
|
|
66
|
+
{ command: "paplay", args: [soundPath] },
|
|
67
|
+
{ command: "aplay", args: [soundPath] },
|
|
68
|
+
{ command: "mpv", args: ["--no-video", "--no-terminal", soundPath] },
|
|
69
|
+
{ command: "ffplay", args: ["-nodisp", "-autoexit", "-loglevel", "quiet", soundPath] },
|
|
70
|
+
]
|
|
71
|
+
|
|
72
|
+
for (const player of players) {
|
|
73
|
+
try {
|
|
74
|
+
await runCommand(player.command, player.args)
|
|
75
|
+
return
|
|
76
|
+
} catch {
|
|
77
|
+
continue
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function playOnMac(soundPath: string): Promise<void> {
|
|
83
|
+
await runCommand("afplay", [soundPath])
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async function playOnWindows(soundPath: string): Promise<void> {
|
|
87
|
+
const script = `& { (New-Object Media.SoundPlayer $args[0]).PlaySync() }`
|
|
88
|
+
await runCommand("powershell", ["-c", script, soundPath])
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export async function playSound(
|
|
92
|
+
event: EventType,
|
|
93
|
+
customPath: string | null
|
|
94
|
+
): Promise<void> {
|
|
95
|
+
const now = Date.now()
|
|
96
|
+
if (lastSoundTime[event] && now - lastSoundTime[event] < DEBOUNCE_MS) {
|
|
97
|
+
return
|
|
98
|
+
}
|
|
99
|
+
lastSoundTime[event] = now
|
|
100
|
+
|
|
101
|
+
const soundPath = getSoundFilePath(event, customPath)
|
|
102
|
+
|
|
103
|
+
if (!soundPath) {
|
|
104
|
+
return
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const os = platform()
|
|
108
|
+
|
|
109
|
+
try {
|
|
110
|
+
switch (os) {
|
|
111
|
+
case "darwin":
|
|
112
|
+
await playOnMac(soundPath)
|
|
113
|
+
break
|
|
114
|
+
case "linux":
|
|
115
|
+
await playOnLinux(soundPath)
|
|
116
|
+
break
|
|
117
|
+
case "win32":
|
|
118
|
+
await playOnWindows(soundPath)
|
|
119
|
+
break
|
|
120
|
+
default:
|
|
121
|
+
break
|
|
122
|
+
}
|
|
123
|
+
} catch {
|
|
124
|
+
// Silent fail - notification will still work
|
|
125
|
+
}
|
|
126
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
// Environment setup & latest features
|
|
4
|
+
"lib": ["ESNext"],
|
|
5
|
+
"target": "ESNext",
|
|
6
|
+
"module": "Preserve",
|
|
7
|
+
"moduleDetection": "force",
|
|
8
|
+
"jsx": "react-jsx",
|
|
9
|
+
"allowJs": true,
|
|
10
|
+
|
|
11
|
+
// Bundler mode
|
|
12
|
+
"moduleResolution": "bundler",
|
|
13
|
+
"allowImportingTsExtensions": true,
|
|
14
|
+
"verbatimModuleSyntax": true,
|
|
15
|
+
"noEmit": true,
|
|
16
|
+
|
|
17
|
+
// Best practices
|
|
18
|
+
"strict": true,
|
|
19
|
+
"skipLibCheck": true,
|
|
20
|
+
"noFallthroughCasesInSwitch": true,
|
|
21
|
+
"noUncheckedIndexedAccess": true,
|
|
22
|
+
"noImplicitOverride": true,
|
|
23
|
+
|
|
24
|
+
// Some stricter flags (disabled by default)
|
|
25
|
+
"noUnusedLocals": false,
|
|
26
|
+
"noUnusedParameters": false,
|
|
27
|
+
"noPropertyAccessFromIndexSignature": false
|
|
28
|
+
}
|
|
29
|
+
}
|