@v-aranda/artifice 1.0.2
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 +39 -0
- package/bin/artifice.js +19 -0
- package/bin/explore.js +39 -0
- package/package.json +32 -0
- package/src/auth/google-auth.js +80 -0
- package/src/notebooklm/client.js +32 -0
- package/src/utils/config.js +27 -0
- package/src/utils/project-config.js +17 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Artifice Team
|
|
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,39 @@
|
|
|
1
|
+
# Artifice CLI
|
|
2
|
+
|
|
3
|
+
CLI para iniciar a etapa de exploração de um projeto com Gemini Notebook Enterprise.
|
|
4
|
+
|
|
5
|
+
## Instalação
|
|
6
|
+
|
|
7
|
+
### Linux (x64)
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
curl -fsSL https://github.com/v-aranda/Artifice/releases/latest/download/install.sh | bash
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
O instalador valida o checksum e instala o executável em `~/.local/bin`, sem precisar de Node.js ou permissões de administrador.
|
|
14
|
+
|
|
15
|
+
### Windows (x64)
|
|
16
|
+
|
|
17
|
+
[Baixar Artifice para Windows](https://github.com/v-aranda/Artifice/releases/latest/download/Artifice-Setup-x64.exe)
|
|
18
|
+
|
|
19
|
+
O instalador adiciona o Artifice ao `PATH` do usuário. Durante a fase beta, o executável ainda não possui assinatura de código e o Windows pode exibir um alerta do SmartScreen.
|
|
20
|
+
|
|
21
|
+
### npm (desenvolvedores)
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
npm install --global @v-aranda/artifice
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Uso
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
artifice explore
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
`artifice explore` configura a autenticação Google, cria o notebook vinculado ao projeto e inclui aqui o link de acesso. O projeto Google Cloud precisa ter o Gemini Notebook Enterprise configurado, licenças atribuídas e permissões IAM adequadas.
|
|
34
|
+
|
|
35
|
+
Para acrescentar uma fonte web ao notebook vinculado:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
artifice source https://example.com --name "Referência"
|
|
39
|
+
```
|
package/bin/artifice.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { program } from 'commander';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import { runExplore } from './explore.js';
|
|
5
|
+
import { configureGoogleAuth } from '../src/auth/google-auth.js';
|
|
6
|
+
import { addWebSource } from '../src/notebooklm/client.js';
|
|
7
|
+
import { getProjectNotebook } from '../src/utils/project-config.js';
|
|
8
|
+
|
|
9
|
+
program.name('artifice').description('CLI para scaffolding e orquestração de agentes de IA').version('1.0.0');
|
|
10
|
+
program.command('explore').description('Cria e configura o notebook do projeto.').option('--force', 'Substitui o vínculo local existente.').action(async (options) => runExplore(options));
|
|
11
|
+
program.command('auth').description('Configura ou renova a autenticação Google.').action(async () => { await configureGoogleAuth(); console.log(chalk.green('✓ Credenciais armazenadas em ~/.artificerc')); });
|
|
12
|
+
program.command('source <url>').description('Adiciona uma URL como fonte ao notebook do projeto.').option('-n, --name <name>', 'Nome exibido da fonte').action(async (url, options) => {
|
|
13
|
+
try { new URL(url); } catch { throw new Error('A fonte deve ser uma URL válida.'); }
|
|
14
|
+
const notebook = getProjectNotebook();
|
|
15
|
+
if (!notebook) throw new Error('Nenhum notebook vinculado. Execute "artifice explore" primeiro.');
|
|
16
|
+
await addWebSource(notebook, url, options.name || url);
|
|
17
|
+
console.log(chalk.green('✓ Fonte adicionada ao notebook.'));
|
|
18
|
+
});
|
|
19
|
+
program.parseAsync(process.argv).catch((error) => { console.error(chalk.red(`\n✗ ${error.message}`)); process.exitCode = 1; });
|
package/bin/explore.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import inquirer from 'inquirer';
|
|
4
|
+
import chalk from 'chalk';
|
|
5
|
+
import { createNotebook } from '../src/notebooklm/client.js';
|
|
6
|
+
import { getProjectNotebook, saveProjectNotebook } from '../src/utils/project-config.js';
|
|
7
|
+
|
|
8
|
+
function writeReadmeLink(notebook, cwd) {
|
|
9
|
+
const file = path.join(cwd, 'README.md');
|
|
10
|
+
const marker = '<!-- artifice:notebook -->';
|
|
11
|
+
const endMarker = '<!-- /artifice:notebook -->';
|
|
12
|
+
const section = `${marker}\n\n## Notebook do projeto\n\n[Abra o notebook no Gemini Notebook Enterprise](${notebook.notebookUrl})\n\nO vínculo técnico do projeto está em \`.artifice/notebook.json\`; credenciais ficam apenas em \`~/.artificerc\`.\n\n${endMarker}`;
|
|
13
|
+
const existing = fs.existsSync(file) ? fs.readFileSync(file, 'utf8') : '# Artifice\n';
|
|
14
|
+
fs.writeFileSync(file, existing.includes(marker) ? existing.replace(new RegExp(`${marker}[\\s\\S]*?${endMarker}`), section) : `${existing.trim()}\n\n${section}\n`, 'utf8');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function writeSpec(notebook, cwd) {
|
|
18
|
+
const file = path.join(cwd, 'SPEC.md');
|
|
19
|
+
if (fs.existsSync(file)) throw new Error(`SPEC.md já existe em ${cwd}. Mova-o ou renomeie-o antes de executar explore.`);
|
|
20
|
+
fs.writeFileSync(file, `# SPEC.md - Configuração do Projeto\n\n## Explore\n\n- **Ferramenta:** Gemini Notebook Enterprise\n- **Notebook:** [${notebook.title}](${notebook.notebookUrl})\n- **Recurso:** \`${notebook.name}\`\n\nAgentes devem usar \`artifice source <url>\` para acrescentar fontes ao notebook vinculado.\n`, 'utf8');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function runExplore({ force = false } = {}) {
|
|
24
|
+
const cwd = process.cwd();
|
|
25
|
+
if (getProjectNotebook(cwd) && !force) throw new Error('Este projeto já possui notebook. Use --force para criar e vincular outro.');
|
|
26
|
+
if (fs.existsSync(path.join(cwd, 'SPEC.md'))) throw new Error(`SPEC.md já existe em ${cwd}. Mova-o ou renomeie-o antes de executar explore.`);
|
|
27
|
+
console.log(chalk.bold.blue('\n🔎 Artifice Explore'));
|
|
28
|
+
const answers = await inquirer.prompt([
|
|
29
|
+
{ type: 'input', name: 'projectNumber', message: 'Número do projeto Google Cloud:', validate: (value) => /^\d+$/.test(value.trim()) || 'Informe o número do projeto (apenas dígitos).' },
|
|
30
|
+
{ type: 'list', name: 'location', message: 'Localização do notebook:', choices: ['global', 'us', 'eu'], default: 'global' },
|
|
31
|
+
{ type: 'list', name: 'endpointRegion', message: 'Multirregião do endpoint da API:', choices: ['global', 'us', 'eu'], default: 'global' },
|
|
32
|
+
{ type: 'input', name: 'title', message: 'Título do notebook:', default: path.basename(cwd) }
|
|
33
|
+
]);
|
|
34
|
+
const notebook = await createNotebook({ ...answers, projectNumber: answers.projectNumber.trim(), title: answers.title.trim() });
|
|
35
|
+
saveProjectNotebook(notebook, cwd);
|
|
36
|
+
writeReadmeLink(notebook, cwd);
|
|
37
|
+
writeSpec(notebook, cwd);
|
|
38
|
+
console.log(chalk.green(`✓ Notebook criado: ${notebook.notebookUrl}`));
|
|
39
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@v-aranda/artifice",
|
|
3
|
+
"version": "1.0.2",
|
|
4
|
+
"description": "CLI para scaffolding e orquestração de agentes de IA",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": { "artifice": "./bin/artifice.js" },
|
|
7
|
+
"scripts": {
|
|
8
|
+
"test": "vitest run",
|
|
9
|
+
"build:standalone": "node scripts/build-standalone.mjs",
|
|
10
|
+
"build:linux": "node scripts/build-standalone.mjs --output release/artifice",
|
|
11
|
+
"build:windows": "node scripts/build-standalone.mjs --output release/artifice.exe"
|
|
12
|
+
},
|
|
13
|
+
"keywords": ["cli", "scaffold", "notebooklm", "agents"],
|
|
14
|
+
"author": "Artifice Team",
|
|
15
|
+
"license": "MIT",
|
|
16
|
+
"repository": { "type": "git", "url": "git+https://github.com/v-aranda/Artifice.git" },
|
|
17
|
+
"bugs": { "url": "https://github.com/v-aranda/Artifice/issues" },
|
|
18
|
+
"homepage": "https://github.com/v-aranda/Artifice#readme",
|
|
19
|
+
"files": ["bin", "src", "README.md", "LICENSE"],
|
|
20
|
+
"engines": { "node": ">=20" },
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"chalk": "^5.3.0",
|
|
23
|
+
"commander": "^11.1.0",
|
|
24
|
+
"googleapis": "^129.0.0",
|
|
25
|
+
"inquirer": "^9.2.12"
|
|
26
|
+
},
|
|
27
|
+
"devDependencies": {
|
|
28
|
+
"esbuild": "^0.25.0",
|
|
29
|
+
"postject": "^1.0.0-alpha.6",
|
|
30
|
+
"vitest": "^1.2.0"
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import http from 'node:http';
|
|
2
|
+
import { execFile } from 'node:child_process';
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import { google } from 'googleapis';
|
|
5
|
+
import inquirer from 'inquirer';
|
|
6
|
+
import { getLocalConfig, saveLocalConfig } from '../utils/config.js';
|
|
7
|
+
|
|
8
|
+
export const GOOGLE_SCOPE = 'https://www.googleapis.com/auth/cloud-platform';
|
|
9
|
+
|
|
10
|
+
function openBrowser(url) {
|
|
11
|
+
const command = process.platform === 'win32' ? 'cmd' : process.platform === 'darwin' ? 'open' : 'xdg-open';
|
|
12
|
+
const args = process.platform === 'win32' ? ['/c', 'start', '', url] : [url];
|
|
13
|
+
execFile(command, args, () => {});
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function waitForAuthorizationCode(server) {
|
|
17
|
+
return new Promise((resolve, reject) => {
|
|
18
|
+
const timeout = setTimeout(() => {
|
|
19
|
+
server.close();
|
|
20
|
+
reject(new Error('Tempo esgotado aguardando o login Google.'));
|
|
21
|
+
}, 180_000);
|
|
22
|
+
server.once('authorization-code', (code) => {
|
|
23
|
+
clearTimeout(timeout);
|
|
24
|
+
resolve(code);
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function authorizeWithOAuth() {
|
|
30
|
+
const answers = await inquirer.prompt([
|
|
31
|
+
{ type: 'input', name: 'clientId', message: 'OAuth Client ID (aplicação Desktop):', validate: (value) => Boolean(value.trim()) || 'Obrigatório.' },
|
|
32
|
+
{ type: 'password', name: 'clientSecret', message: 'OAuth Client Secret:', mask: '*', validate: (value) => Boolean(value.trim()) || 'Obrigatório.' }
|
|
33
|
+
]);
|
|
34
|
+
const server = http.createServer((request, response) => {
|
|
35
|
+
const url = new URL(request.url, 'http://127.0.0.1');
|
|
36
|
+
const code = url.searchParams.get('code');
|
|
37
|
+
response.writeHead(code ? 200 : 400, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
38
|
+
response.end(code ? '<h1>Autorização concluída.</h1><p>Você pode fechar esta janela.</p>' : '<h1>Falha na autorização.</h1>');
|
|
39
|
+
if (code) server.emit('authorization-code', code);
|
|
40
|
+
});
|
|
41
|
+
await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));
|
|
42
|
+
const redirectUri = `http://127.0.0.1:${server.address().port}/oauth2callback`;
|
|
43
|
+
const client = new google.auth.OAuth2(answers.clientId.trim(), answers.clientSecret.trim(), redirectUri);
|
|
44
|
+
const url = client.generateAuthUrl({ access_type: 'offline', prompt: 'consent', scope: [GOOGLE_SCOPE] });
|
|
45
|
+
console.log(`\nAbra este endereço para autenticar:\n${url}\n`);
|
|
46
|
+
openBrowser(url);
|
|
47
|
+
const code = await waitForAuthorizationCode(server);
|
|
48
|
+
server.close();
|
|
49
|
+
const { tokens } = await client.getToken(code);
|
|
50
|
+
client.setCredentials(tokens);
|
|
51
|
+
return { method: 'oauth', clientId: answers.clientId.trim(), clientSecret: answers.clientSecret.trim(), redirectUri, tokens, updatedAt: new Date().toISOString() };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function configureServiceAccount() {
|
|
55
|
+
const { filePath } = await inquirer.prompt([{ type: 'input', name: 'filePath', message: 'Caminho absoluto do JSON da Service Account:', validate: (value) => fs.existsSync(value.trim()) || 'Arquivo não encontrado.' }]);
|
|
56
|
+
return { method: 'service_account', keyFile: filePath.trim(), updatedAt: new Date().toISOString() };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export async function configureGoogleAuth() {
|
|
60
|
+
const { method } = await inquirer.prompt([{ type: 'list', name: 'method', message: 'Método de autenticação Google:', choices: [
|
|
61
|
+
{ name: 'OAuth 2.0 (recomendado para notebook de um usuário)', value: 'oauth' },
|
|
62
|
+
{ name: 'Service Account (automação no Google Cloud)', value: 'service_account' }
|
|
63
|
+
] }]);
|
|
64
|
+
const credentials = method === 'oauth' ? await authorizeWithOAuth() : await configureServiceAccount();
|
|
65
|
+
saveLocalConfig({ ...getLocalConfig(), google: credentials });
|
|
66
|
+
return credentials;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export async function getGoogleAuth(forceConfigure = false) {
|
|
70
|
+
let credentials = forceConfigure ? null : getLocalConfig().google;
|
|
71
|
+
if (!credentials) credentials = await configureGoogleAuth();
|
|
72
|
+
if (credentials.method === 'service_account') return new google.auth.GoogleAuth({ keyFile: credentials.keyFile, scopes: [GOOGLE_SCOPE] });
|
|
73
|
+
const client = new google.auth.OAuth2(credentials.clientId, credentials.clientSecret, credentials.redirectUri);
|
|
74
|
+
client.setCredentials(credentials.tokens);
|
|
75
|
+
client.on('tokens', (tokens) => {
|
|
76
|
+
credentials.tokens = { ...credentials.tokens, ...tokens };
|
|
77
|
+
saveLocalConfig({ ...getLocalConfig(), google: credentials });
|
|
78
|
+
});
|
|
79
|
+
return client;
|
|
80
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { getGoogleAuth } from '../auth/google-auth.js';
|
|
2
|
+
|
|
3
|
+
function endpoint(region) {
|
|
4
|
+
const prefix = region === 'global' ? '' : `${region}-`;
|
|
5
|
+
return `https://${prefix}discoveryengine.googleapis.com/v1alpha`;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function notebookUrl({ projectNumber, location, notebookId }) {
|
|
9
|
+
return `https://notebook.cloud.google.com/${location}/notebook/${notebookId}?project=${projectNumber}`;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export async function createNotebook({ projectNumber, location, endpointRegion, title }) {
|
|
13
|
+
const auth = await getGoogleAuth();
|
|
14
|
+
const token = await auth.getAccessToken();
|
|
15
|
+
const response = await fetch(`${endpoint(endpointRegion)}/projects/${projectNumber}/locations/${location}/notebooks`, {
|
|
16
|
+
method: 'POST', headers: { Authorization: `Bearer ${token.token || token}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ title })
|
|
17
|
+
});
|
|
18
|
+
const body = await response.json();
|
|
19
|
+
if (!response.ok) throw new Error(body.error?.message || `Google API retornou ${response.status}.`);
|
|
20
|
+
return { ...body, projectNumber, location, endpointRegion, notebookUrl: notebookUrl({ projectNumber, location, notebookId: body.notebookId }) };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function addWebSource(notebook, url, sourceName) {
|
|
24
|
+
const auth = await getGoogleAuth();
|
|
25
|
+
const token = await auth.getAccessToken();
|
|
26
|
+
const response = await fetch(`${endpoint(notebook.endpointRegion)}/${notebook.name}/sources:batchCreate`, {
|
|
27
|
+
method: 'POST', headers: { Authorization: `Bearer ${token.token || token}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ userContents: [{ webContent: { url, sourceName } }] })
|
|
28
|
+
});
|
|
29
|
+
const body = await response.json();
|
|
30
|
+
if (!response.ok) throw new Error(body.error?.message || `Google API retornou ${response.status}.`);
|
|
31
|
+
return body;
|
|
32
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
|
|
5
|
+
export const getConfigPath = () => process.env.ARTIFICE_CONFIG_PATH || path.join(os.homedir(), '.artificerc');
|
|
6
|
+
|
|
7
|
+
export function getLocalConfig() {
|
|
8
|
+
const configPath = getConfigPath();
|
|
9
|
+
if (!fs.existsSync(configPath)) return {};
|
|
10
|
+
try {
|
|
11
|
+
return JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
12
|
+
} catch {
|
|
13
|
+
return {};
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function saveLocalConfig(config) {
|
|
18
|
+
const configPath = getConfigPath();
|
|
19
|
+
fs.mkdirSync(path.dirname(configPath), { recursive: true });
|
|
20
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2), { encoding: 'utf8', mode: 0o600 });
|
|
21
|
+
try { fs.chmodSync(configPath, 0o600); } catch { /* Windows does not support POSIX modes. */ }
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function clearLocalConfig() {
|
|
25
|
+
const configPath = getConfigPath();
|
|
26
|
+
if (fs.existsSync(configPath)) fs.unlinkSync(configPath);
|
|
27
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
const projectConfigPath = (cwd = process.cwd()) => path.join(cwd, '.artifice', 'notebook.json');
|
|
5
|
+
|
|
6
|
+
export function getProjectNotebook(cwd) {
|
|
7
|
+
const file = projectConfigPath(cwd);
|
|
8
|
+
if (!fs.existsSync(file)) return null;
|
|
9
|
+
return JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function saveProjectNotebook(notebook, cwd) {
|
|
13
|
+
const file = projectConfigPath(cwd);
|
|
14
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
15
|
+
fs.writeFileSync(file, JSON.stringify(notebook, null, 2), 'utf8');
|
|
16
|
+
return file;
|
|
17
|
+
}
|